diff --git a/docs/cli/settings.md b/docs/cli/settings.md index c17e9898ce..58ba252ec9 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -50,6 +50,7 @@ they appear in the UI. | Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` | | Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` | | Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` | +| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` | | Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` | | Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` | | Hide Banner | `ui.hideBanner` | Hide the application banner | `false` | diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 26831693db..8d324c8795 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -233,6 +233,11 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `true` - **Requires restart:** Yes +- **`ui.showCompatibilityWarnings`** (boolean): + - **Description:** Show warnings about terminal or OS compatibility issues. + - **Default:** `true` + - **Requires restart:** Yes + - **`ui.hideTips`** (boolean): - **Description:** Hide helpful tips in the UI - **Default:** `false` diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index a684b5553a..40dd6a10f2 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -485,6 +485,15 @@ const SETTINGS_SCHEMA = { 'Show a warning when running Gemini CLI in the home directory.', showInDialog: true, }, + showCompatibilityWarnings: { + type: 'boolean', + label: 'Show Compatibility Warnings', + category: 'UI', + requiresRestart: true, + default: true, + description: 'Show warnings about terminal or OS compatibility issues.', + showInDialog: true, + }, hideTips: { type: 'boolean', label: 'Hide Tips', diff --git a/packages/cli/src/ui/components/Notifications.tsx b/packages/cli/src/ui/components/Notifications.tsx index c252dd12de..1573ef682c 100644 --- a/packages/cli/src/ui/components/Notifications.tsx +++ b/packages/cli/src/ui/components/Notifications.tsx @@ -88,17 +88,16 @@ export const Notifications = () => { )} {updateInfo && } {showStartupWarnings && ( - + {startupWarnings.map((warning, index) => ( - - {warning} - + + + + + + {warning} + + ))} )} diff --git a/packages/cli/src/utils/userStartupWarnings.test.ts b/packages/cli/src/utils/userStartupWarnings.test.ts index c3746ad0a1..131d77f580 100644 --- a/packages/cli/src/utils/userStartupWarnings.test.ts +++ b/packages/cli/src/utils/userStartupWarnings.test.ts @@ -13,6 +13,7 @@ import { isFolderTrustEnabled, isWorkspaceTrusted, } from '../config/trustedFolders.js'; +import { getCompatibilityWarnings } from '@google/gemini-cli-core'; // Mock os.homedir to control the home directory in tests vi.mock('os', async (importOriginal) => { @@ -29,6 +30,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { return { ...actual, homedir: () => os.homedir(), + getCompatibilityWarnings: vi.fn().mockReturnValue([]), }; }); @@ -51,11 +53,12 @@ describe('getUserStartupWarnings', () => { isTrusted: false, source: undefined, }); + vi.mocked(getCompatibilityWarnings).mockReturnValue([]); }); afterEach(async () => { await fs.rm(testRootDir, { recursive: true, force: true }); - vi.clearAllMocks(); + vi.restoreAllMocks(); }); describe('home directory check', () => { @@ -135,4 +138,27 @@ describe('getUserStartupWarnings', () => { expect(warnings).toEqual([expectedWarning, expectedWarning]); }); }); + + describe('compatibility warnings', () => { + it('should include compatibility warnings by default', async () => { + vi.mocked(getCompatibilityWarnings).mockReturnValue(['Comp warning 1']); + const projectDir = path.join(testRootDir, 'project'); + await fs.mkdir(projectDir); + + const warnings = await getUserStartupWarnings({}, projectDir); + expect(warnings).toContain('Comp warning 1'); + }); + + it('should not include compatibility warnings when showCompatibilityWarnings is false', async () => { + vi.mocked(getCompatibilityWarnings).mockReturnValue(['Comp warning 1']); + const projectDir = path.join(testRootDir, 'project'); + await fs.mkdir(projectDir); + + const warnings = await getUserStartupWarnings( + { ui: { showCompatibilityWarnings: false } }, + projectDir, + ); + expect(warnings).not.toContain('Comp warning 1'); + }); + }); }); diff --git a/packages/cli/src/utils/userStartupWarnings.ts b/packages/cli/src/utils/userStartupWarnings.ts index 8a0c0b0d3a..cc2d2638d6 100644 --- a/packages/cli/src/utils/userStartupWarnings.ts +++ b/packages/cli/src/utils/userStartupWarnings.ts @@ -7,7 +7,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import process from 'node:process'; -import { homedir } from '@google/gemini-cli-core'; +import { homedir, getCompatibilityWarnings } from '@google/gemini-cli-core'; import type { Settings } from '../config/settingsSchema.js'; import { isFolderTrustEnabled, @@ -84,5 +84,11 @@ export async function getUserStartupWarnings( const results = await Promise.all( WARNING_CHECKS.map((check) => check.check(workspaceRoot, settings)), ); - return results.filter((msg) => msg !== null); + const warnings = results.filter((msg) => msg !== null); + + if (settings.ui?.showCompatibilityWarnings !== false) { + warnings.push(...getCompatibilityWarnings()); + } + + return warnings; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 95b8d41c29..8f82486173 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -180,6 +180,7 @@ export { OAuthUtils } from './mcp/oauth-utils.js'; // Export telemetry functions export * from './telemetry/index.js'; export { sessionId } from './utils/session.js'; +export * from './utils/compatibility.js'; export * from './utils/browser.js'; export { Storage } from './config/storage.js'; diff --git a/packages/core/src/utils/compatibility.test.ts b/packages/core/src/utils/compatibility.test.ts new file mode 100644 index 0000000000..8db512292a --- /dev/null +++ b/packages/core/src/utils/compatibility.test.ts @@ -0,0 +1,156 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import os from 'node:os'; +import { + isWindows10, + isJetBrainsTerminal, + supportsTrueColor, + getCompatibilityWarnings, +} from './compatibility.js'; + +vi.mock('node:os', () => ({ + default: { + platform: vi.fn(), + release: vi.fn(), + }, +})); + +describe('compatibility', () => { + const originalGetColorDepth = process.stdout.getColorDepth; + + afterEach(() => { + process.stdout.getColorDepth = originalGetColorDepth; + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + describe('isWindows10', () => { + it('should return true for Windows 10 (build < 22000)', () => { + vi.mocked(os.platform).mockReturnValue('win32'); + vi.mocked(os.release).mockReturnValue('10.0.19041'); + expect(isWindows10()).toBe(true); + }); + + it('should return false for Windows 11 (build >= 22000)', () => { + vi.mocked(os.platform).mockReturnValue('win32'); + vi.mocked(os.release).mockReturnValue('10.0.22000'); + expect(isWindows10()).toBe(false); + }); + + it('should return false for non-Windows platforms', () => { + vi.mocked(os.platform).mockReturnValue('darwin'); + vi.mocked(os.release).mockReturnValue('20.6.0'); + expect(isWindows10()).toBe(false); + }); + }); + + describe('isJetBrainsTerminal', () => { + it('should return true when TERMINAL_EMULATOR is JetBrains-JediTerm', () => { + vi.stubEnv('TERMINAL_EMULATOR', 'JetBrains-JediTerm'); + expect(isJetBrainsTerminal()).toBe(true); + }); + + it('should return false for other terminals', () => { + vi.stubEnv('TERMINAL_EMULATOR', 'something-else'); + expect(isJetBrainsTerminal()).toBe(false); + }); + + it('should return false when TERMINAL_EMULATOR is not set', () => { + vi.stubEnv('TERMINAL_EMULATOR', ''); + expect(isJetBrainsTerminal()).toBe(false); + }); + }); + + describe('supportsTrueColor', () => { + it('should return true when COLORTERM is truecolor', () => { + vi.stubEnv('COLORTERM', 'truecolor'); + expect(supportsTrueColor()).toBe(true); + }); + + it('should return true when COLORTERM is 24bit', () => { + vi.stubEnv('COLORTERM', '24bit'); + expect(supportsTrueColor()).toBe(true); + }); + + it('should return true when getColorDepth returns >= 24', () => { + vi.stubEnv('COLORTERM', ''); + process.stdout.getColorDepth = vi.fn().mockReturnValue(24); + expect(supportsTrueColor()).toBe(true); + }); + + it('should return false when true color is not supported', () => { + vi.stubEnv('COLORTERM', ''); + process.stdout.getColorDepth = vi.fn().mockReturnValue(8); + expect(supportsTrueColor()).toBe(false); + }); + }); + + describe('getCompatibilityWarnings', () => { + beforeEach(() => { + // Default to supporting true color to keep existing tests simple + vi.stubEnv('COLORTERM', 'truecolor'); + process.stdout.getColorDepth = vi.fn().mockReturnValue(24); + }); + + it('should return Windows 10 warning when detected', () => { + vi.mocked(os.platform).mockReturnValue('win32'); + vi.mocked(os.release).mockReturnValue('10.0.19041'); + vi.stubEnv('TERMINAL_EMULATOR', ''); + + const warnings = getCompatibilityWarnings(); + expect(warnings).toContain( + 'Warning: Windows 10 detected. Some UI features like smooth scrolling may be degraded. Windows 11 is recommended for the best experience.', + ); + }); + + it('should return JetBrains warning when detected', () => { + vi.mocked(os.platform).mockReturnValue('darwin'); + vi.stubEnv('TERMINAL_EMULATOR', 'JetBrains-JediTerm'); + + const warnings = getCompatibilityWarnings(); + expect(warnings).toContain( + 'Warning: JetBrains terminal detected. You may experience rendering or scrolling issues. Using an external terminal (e.g., Windows Terminal, iTerm2) is recommended.', + ); + }); + + it('should return true color warning when not supported', () => { + vi.mocked(os.platform).mockReturnValue('darwin'); + vi.stubEnv('TERMINAL_EMULATOR', ''); + vi.stubEnv('COLORTERM', ''); + process.stdout.getColorDepth = vi.fn().mockReturnValue(8); + + const warnings = getCompatibilityWarnings(); + expect(warnings).toContain( + 'Warning: True color (24-bit) support not detected. Using a terminal with true color enabled will result in a better visual experience.', + ); + }); + + it('should return all warnings when all are detected', () => { + vi.mocked(os.platform).mockReturnValue('win32'); + vi.mocked(os.release).mockReturnValue('10.0.19041'); + vi.stubEnv('TERMINAL_EMULATOR', 'JetBrains-JediTerm'); + vi.stubEnv('COLORTERM', ''); + process.stdout.getColorDepth = vi.fn().mockReturnValue(8); + + const warnings = getCompatibilityWarnings(); + expect(warnings).toHaveLength(3); + expect(warnings[0]).toContain('Windows 10 detected'); + expect(warnings[1]).toContain('JetBrains terminal detected'); + expect(warnings[2]).toContain('True color (24-bit) support not detected'); + }); + + it('should return no warnings in a standard environment with true color', () => { + vi.mocked(os.platform).mockReturnValue('darwin'); + vi.stubEnv('TERMINAL_EMULATOR', ''); + vi.stubEnv('COLORTERM', 'truecolor'); + + const warnings = getCompatibilityWarnings(); + expect(warnings).toHaveLength(0); + }); + }); +}); diff --git a/packages/core/src/utils/compatibility.ts b/packages/core/src/utils/compatibility.ts new file mode 100644 index 0000000000..b1b6240a65 --- /dev/null +++ b/packages/core/src/utils/compatibility.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import os from 'node:os'; + +/** + * Detects if the current OS is Windows 10. + * Windows 11 also reports as version 10.0, but with build numbers >= 22000. + */ +export function isWindows10(): boolean { + if (os.platform() !== 'win32') { + return false; + } + const release = os.release(); + const parts = release.split('.'); + if (parts.length >= 3 && parts[0] === '10' && parts[1] === '0') { + const build = parseInt(parts[2], 10); + return build < 22000; + } + return false; +} + +/** + * Detects if the current terminal is a JetBrains-based IDE terminal. + */ +export function isJetBrainsTerminal(): boolean { + return process.env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm'; +} + +/** + * Detects if the current terminal supports true color (24-bit). + */ +export function supportsTrueColor(): boolean { + // Check COLORTERM environment variable + if ( + process.env['COLORTERM'] === 'truecolor' || + process.env['COLORTERM'] === '24bit' + ) { + return true; + } + + // Check if stdout supports 24-bit color depth + if (process.stdout.getColorDepth && process.stdout.getColorDepth() >= 24) { + return true; + } + + return false; +} + +/** + * Returns a list of compatibility warnings based on the current environment. + */ +export function getCompatibilityWarnings(): string[] { + const warnings: string[] = []; + + if (isWindows10()) { + warnings.push( + 'Warning: Windows 10 detected. Some UI features like smooth scrolling may be degraded. Windows 11 is recommended for the best experience.', + ); + } + + if (isJetBrainsTerminal()) { + warnings.push( + 'Warning: JetBrains terminal detected. You may experience rendering or scrolling issues. Using an external terminal (e.g., Windows Terminal, iTerm2) is recommended.', + ); + } + + if (!supportsTrueColor()) { + warnings.push( + 'Warning: True color (24-bit) support not detected. Using a terminal with true color enabled will result in a better visual experience.', + ); + } + + return warnings; +} diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index f885990a58..7847323ea2 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -262,6 +262,13 @@ "default": true, "type": "boolean" }, + "showCompatibilityWarnings": { + "title": "Show Compatibility Warnings", + "description": "Show warnings about terminal or OS compatibility issues.", + "markdownDescription": "Show warnings about terminal or OS compatibility issues.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, + "type": "boolean" + }, "hideTips": { "title": "Hide Tips", "description": "Hide helpful tips in the UI",