Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -671,14 +671,20 @@ export async function main() {
}

let input = config.getQuestion();
const useAlternateBuffer = shouldEnterAlternateScreen(
isAlternateBufferEnabled(settings),
config.getScreenReader(),
);
const rawStartupWarnings = await getStartupWarnings();
const startupWarnings: StartupWarning[] = [
...rawStartupWarnings.map((message) => ({
id: `startup-${createHash('sha256').update(message).digest('hex').substring(0, 16)}`,
message,
priority: WarningPriority.High,
})),
...(await getUserStartupWarnings(settings.merged)),
...(await getUserStartupWarnings(settings.merged, undefined, {
isAlternateBuffer: useAlternateBuffer,
})),
];

// Handle --resume flag
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/utils/userStartupWarnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const WARNING_CHECKS: readonly WarningCheck[] = [
export async function getUserStartupWarnings(
settings: Settings,
workspaceRoot: string = process.cwd(),
options?: { isAlternateBuffer?: boolean },
): Promise<StartupWarning[]> {
const results = await Promise.all(
WARNING_CHECKS.map(async (check) => {
Expand All @@ -105,7 +106,11 @@ export async function getUserStartupWarnings(
const warnings = results.filter((w): w is StartupWarning => w !== null);

if (settings.ui?.showCompatibilityWarnings !== false) {
warnings.push(...getCompatibilityWarnings());
warnings.push(
...getCompatibilityWarnings({
isAlternateBuffer: options?.isAlternateBuffer,
}),
);
}

return warnings;
Expand Down
243 changes: 165 additions & 78 deletions packages/core/src/utils/compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
supports256Colors,
supportsTrueColor,
getCompatibilityWarnings,
WarningPriority,
} from './compatibility.js';

vi.mock('node:os', () => ({
Expand All @@ -31,83 +32,128 @@ describe('compatibility', () => {
});

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);
});
it.each<{
platform: NodeJS.Platform;
release: string;
expected: boolean;
desc: string;
}>([
{
platform: 'win32',
release: '10.0.19041',
expected: true,
desc: 'Windows 10 (build < 22000)',
},
{
platform: 'win32',
release: '10.0.22000',
expected: false,
desc: 'Windows 11 (build >= 22000)',
},
{
platform: 'darwin',
release: '20.6.0',
expected: false,
desc: 'non-Windows platforms',
},
])(
'should return $expected for $desc',
({ platform, release, expected }) => {
vi.mocked(os.platform).mockReturnValue(platform);
vi.mocked(os.release).mockReturnValue(release);
expect(isWindows10()).toBe(expected);
},
);
});

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);
it.each<{ env: string; expected: boolean; desc: string }>([
{
env: 'JetBrains-JediTerm',
expected: true,
desc: 'TERMINAL_EMULATOR is JetBrains-JediTerm',
},
{ env: 'something-else', expected: false, desc: 'other terminals' },
{ env: '', expected: false, desc: 'TERMINAL_EMULATOR is not set' },
])('should return $expected when $desc', ({ env, expected }) => {
vi.stubEnv('TERMINAL_EMULATOR', env);
expect(isJetBrainsTerminal()).toBe(expected);
});
});

describe('supports256Colors', () => {
it('should return true when getColorDepth returns >= 8', () => {
process.stdout.getColorDepth = vi.fn().mockReturnValue(8);
expect(supports256Colors()).toBe(true);
});

it('should return true when TERM contains 256color', () => {
process.stdout.getColorDepth = vi.fn().mockReturnValue(4);
vi.stubEnv('TERM', 'xterm-256color');
expect(supports256Colors()).toBe(true);
});

it('should return false when 256 colors are not supported', () => {
process.stdout.getColorDepth = vi.fn().mockReturnValue(4);
vi.stubEnv('TERM', 'xterm');
expect(supports256Colors()).toBe(false);
it.each<{
depth: number;
term?: string;
expected: boolean;
desc: string;
}>([
{
depth: 8,
term: undefined,
expected: true,
desc: 'getColorDepth returns >= 8',
},
{
depth: 4,
term: 'xterm-256color',
expected: true,
desc: 'TERM contains 256color',
},
{
depth: 4,
term: 'xterm',
expected: false,
desc: '256 colors are not supported',
},
])('should return $expected when $desc', ({ depth, term, expected }) => {
process.stdout.getColorDepth = vi.fn().mockReturnValue(depth);
if (term !== undefined) {
vi.stubEnv('TERM', term);
}
expect(supports256Colors()).toBe(expected);
});
});

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);
});
it.each<{
colorterm: string;
depth: number;
expected: boolean;
desc: string;
}>([
{
colorterm: 'truecolor',
depth: 8,
expected: true,
desc: 'COLORTERM is truecolor',
},
{
colorterm: '24bit',
depth: 8,
expected: true,
desc: 'COLORTERM is 24bit',
},
{
colorterm: '',
depth: 24,
expected: true,
desc: 'getColorDepth returns >= 24',
},
{
colorterm: '',
depth: 8,
expected: false,
desc: 'true color is not supported',
},
])(
'should return $expected when $desc',
({ colorterm, depth, expected }) => {
vi.stubEnv('COLORTERM', colorterm);
process.stdout.getColorDepth = vi.fn().mockReturnValue(depth);
expect(supportsTrueColor()).toBe(expected);
},
);
});

describe('getCompatibilityWarnings', () => {
Expand All @@ -131,17 +177,58 @@ describe('compatibility', () => {
);
});

it('should return JetBrains warning when detected', () => {
it.each<{
platform: NodeJS.Platform;
release: string;
externalTerminal: string;
desc: string;
}>([
{
platform: 'darwin',
release: '20.6.0',
externalTerminal: 'iTerm2 or Ghostty',
desc: 'macOS',
},
{
platform: 'win32',
release: '10.0.22000',
externalTerminal: 'Windows Terminal',
desc: 'Windows',
}, // Valid Windows 11 release to not trigger the Windows 10 warning
{
platform: 'linux',
release: '5.10.0',
externalTerminal: 'Ghostty',
desc: 'Linux',
},
])(
'should return JetBrains warning when detected and in alternate buffer ($desc)',
({ platform, release, externalTerminal }) => {
vi.mocked(os.platform).mockReturnValue(platform);
vi.mocked(os.release).mockReturnValue(release);
vi.stubEnv('TERMINAL_EMULATOR', 'JetBrains-JediTerm');

const warnings = getCompatibilityWarnings({ isAlternateBuffer: true });
expect(warnings).toContainEqual(
expect.objectContaining({
id: 'jetbrains-terminal',
message: expect.stringContaining(
`Warning: JetBrains mouse scrolling is unreliable. Disabling alternate buffer mode in settings or using an external terminal (e.g., ${externalTerminal}) is recommended.`,
),
priority: WarningPriority.High,
}),
);
},
);

it('should not return JetBrains warning when detected but NOT in alternate buffer', () => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.stubEnv('TERMINAL_EMULATOR', 'JetBrains-JediTerm');

const warnings = getCompatibilityWarnings();
expect(warnings).toContainEqual(
expect.objectContaining({
id: 'jetbrains-terminal',
message: expect.stringContaining('JetBrains terminal detected'),
}),
);
const warnings = getCompatibilityWarnings({ isAlternateBuffer: false });
expect(
warnings.find((w) => w.id === 'jetbrains-terminal'),
).toBeUndefined();
});

it('should return 256-color warning when 256 colors are not supported', () => {
Expand All @@ -156,7 +243,7 @@ describe('compatibility', () => {
expect.objectContaining({
id: '256-color',
message: expect.stringContaining('256-color support not detected'),
priority: 'high',
priority: WarningPriority.High,
}),
);
// Should NOT show true-color warning if 256-color warning is shown
Expand All @@ -177,7 +264,7 @@ describe('compatibility', () => {
message: expect.stringContaining(
'True color (24-bit) support not detected',
),
priority: 'low',
priority: WarningPriority.Low,
}),
);
});
Expand All @@ -201,10 +288,10 @@ describe('compatibility', () => {
vi.stubEnv('TERM_PROGRAM', 'xterm');
process.stdout.getColorDepth = vi.fn().mockReturnValue(8);

const warnings = getCompatibilityWarnings();
const warnings = getCompatibilityWarnings({ isAlternateBuffer: true });
expect(warnings).toHaveLength(3);
expect(warnings[0].message).toContain('Windows 10 detected');
expect(warnings[1].message).toContain('JetBrains terminal detected');
expect(warnings[1].message).toContain('JetBrains');
expect(warnings[2].message).toContain(
'True color (24-bit) support not detected',
);
Expand Down
23 changes: 16 additions & 7 deletions packages/core/src/utils/compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ export function supportsTrueColor(): boolean {
return false;
}

/**
* Returns a list of compatibility warnings based on the current environment.
*/
export enum WarningPriority {
Low = 'low',
High = 'high',
Expand All @@ -89,7 +86,12 @@ export interface StartupWarning {
priority: WarningPriority;
}

export function getCompatibilityWarnings(): StartupWarning[] {
/**
* Returns a list of compatibility warnings based on the current environment.
*/
export function getCompatibilityWarnings(options?: {
isAlternateBuffer?: boolean;
}): StartupWarning[] {
const warnings: StartupWarning[] = [];

if (isWindows10()) {
Expand All @@ -101,11 +103,18 @@ export function getCompatibilityWarnings(): StartupWarning[] {
});
}

if (isJetBrainsTerminal()) {
if (isJetBrainsTerminal() && options?.isAlternateBuffer) {
const platformTerminals: Partial<Record<NodeJS.Platform, string>> = {
win32: 'Windows Terminal',
darwin: 'iTerm2 or Ghostty',
linux: 'Ghostty',
};
const suggestion = platformTerminals[os.platform()];
const suggestedTerminals = suggestion ? ` (e.g., ${suggestion})` : '';

warnings.push({
id: 'jetbrains-terminal',
message:
'Warning: JetBrains terminal detected. You may experience rendering or scrolling issues. Using an external terminal (e.g., Windows Terminal, iTerm2) is recommended.',
message: `Warning: JetBrains mouse scrolling is unreliable. Disabling alternate buffer mode in settings or using an external terminal${suggestedTerminals} is recommended.`,
priority: WarningPriority.High,
});
}
Expand Down
Loading