mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-20 08:42:39 -07:00
fix(core): add actionable warnings for terminal fallbacks (#14426)
Adds detection for tmux, GNU screen, and dumb terminals. Provides actionable warnings without artificially restricting features like alternate screen buffer or truecolor overrides.
This commit is contained in:
@@ -9,6 +9,11 @@ import os from 'node:os';
|
||||
import {
|
||||
isWindows10,
|
||||
isJetBrainsTerminal,
|
||||
isTmux,
|
||||
isGnuScreen,
|
||||
isLowColorTmux,
|
||||
isDumbTerminal,
|
||||
getTerminalNameFromEnv,
|
||||
supports256Colors,
|
||||
supportsTrueColor,
|
||||
getCompatibilityWarnings,
|
||||
@@ -67,20 +72,120 @@ describe('compatibility', () => {
|
||||
});
|
||||
|
||||
describe('isJetBrainsTerminal', () => {
|
||||
it.each<{ env: string; expected: boolean; desc: string }>([
|
||||
it.each<{
|
||||
env: Record<string, string>;
|
||||
expected: boolean;
|
||||
desc: string;
|
||||
}>([
|
||||
{
|
||||
env: 'JetBrains-JediTerm',
|
||||
env: { TERMINAL_EMULATOR: 'JetBrains-JediTerm' },
|
||||
expected: true,
|
||||
desc: 'TERMINAL_EMULATOR is JetBrains-JediTerm',
|
||||
desc: 'TERMINAL_EMULATOR starts with JetBrains',
|
||||
},
|
||||
{ env: 'something-else', expected: false, desc: 'other terminals' },
|
||||
{ env: '', expected: false, desc: 'TERMINAL_EMULATOR is not set' },
|
||||
{
|
||||
env: { JETBRAINS_IDE: 'IntelliJ' },
|
||||
expected: true,
|
||||
desc: 'JETBRAINS_IDE is set',
|
||||
},
|
||||
{
|
||||
env: { TERMINAL_EMULATOR: 'xterm' },
|
||||
expected: false,
|
||||
desc: 'other terminals',
|
||||
},
|
||||
{ env: {}, expected: false, desc: 'no env vars set' },
|
||||
])('should return $expected when $desc', ({ env, expected }) => {
|
||||
vi.stubEnv('TERMINAL_EMULATOR', env);
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
vi.stubEnv(key, value);
|
||||
}
|
||||
expect(isJetBrainsTerminal()).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTmux', () => {
|
||||
it('should return true when TMUX is set', () => {
|
||||
vi.stubEnv('TMUX', '/tmp/tmux-1001/default,1425,0');
|
||||
expect(isTmux()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when TMUX is not set', () => {
|
||||
expect(isTmux()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGnuScreen', () => {
|
||||
it('should return true when STY is set', () => {
|
||||
vi.stubEnv('STY', '1234.pts-0.host');
|
||||
expect(isGnuScreen()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when STY is not set', () => {
|
||||
expect(isGnuScreen()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLowColorTmux', () => {
|
||||
it('should return true when TERM=screen and COLORTERM is not set', () => {
|
||||
vi.stubEnv('TERM', 'screen');
|
||||
vi.stubEnv('COLORTERM', '');
|
||||
expect(isLowColorTmux()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when TERM=screen and COLORTERM is set', () => {
|
||||
vi.stubEnv('TERM', 'screen');
|
||||
vi.stubEnv('COLORTERM', 'truecolor');
|
||||
expect(isLowColorTmux()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when TERM=xterm-256color', () => {
|
||||
vi.stubEnv('TERM', 'xterm-256color');
|
||||
expect(isLowColorTmux()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDumbTerminal', () => {
|
||||
it('should return true when TERM=dumb', () => {
|
||||
vi.stubEnv('TERM', 'dumb');
|
||||
expect(isDumbTerminal()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when TERM=vt100', () => {
|
||||
vi.stubEnv('TERM', 'vt100');
|
||||
expect(isDumbTerminal()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when TERM=xterm', () => {
|
||||
vi.stubEnv('TERM', 'xterm');
|
||||
expect(isDumbTerminal()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTerminalNameFromEnv', () => {
|
||||
it('should prioritize TERM_PROGRAM', () => {
|
||||
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
|
||||
expect(getTerminalNameFromEnv()).toBe('iTerm.app');
|
||||
});
|
||||
|
||||
it('should use JETBRAINS_IDE if TERM_PROGRAM is missing', () => {
|
||||
vi.stubEnv('TERMINAL_EMULATOR', 'JetBrains-JediTerm');
|
||||
vi.stubEnv('JETBRAINS_IDE', 'PyCharm');
|
||||
expect(getTerminalNameFromEnv()).toBe('PyCharm');
|
||||
});
|
||||
|
||||
it('should use TMUX if other vars are missing', () => {
|
||||
vi.stubEnv('TMUX', 'some-socket');
|
||||
expect(getTerminalNameFromEnv()).toBe('tmux');
|
||||
});
|
||||
|
||||
it('should use STY if other vars are missing', () => {
|
||||
vi.stubEnv('STY', 'some-session');
|
||||
expect(getTerminalNameFromEnv()).toBe('GNU screen');
|
||||
});
|
||||
|
||||
it('should return Unknown if no vars are set', () => {
|
||||
expect(getTerminalNameFromEnv()).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('supports256Colors', () => {
|
||||
it.each<{
|
||||
depth: number;
|
||||
@@ -177,49 +282,72 @@ describe('compatibility', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
it('should return JetBrains warning when detected and in alternate buffer', () => {
|
||||
vi.mocked(os.platform).mockReturnValue('darwin');
|
||||
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,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
const warnings = getCompatibilityWarnings({ isAlternateBuffer: true });
|
||||
expect(warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'jetbrains-terminal',
|
||||
message: expect.stringContaining('JetBrains terminal detected'),
|
||||
priority: WarningPriority.High,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return tmux warning when detected and in alternate buffer', () => {
|
||||
vi.stubEnv('TMUX', '/tmp/tmux-1001/default,1,0');
|
||||
|
||||
const warnings = getCompatibilityWarnings({ isAlternateBuffer: true });
|
||||
expect(warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'tmux-alternate-buffer',
|
||||
message: expect.stringContaining('tmux detected'),
|
||||
priority: WarningPriority.High,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return low-color tmux warning when detected', () => {
|
||||
vi.stubEnv('TERM', 'screen');
|
||||
vi.stubEnv('COLORTERM', '');
|
||||
|
||||
const warnings = getCompatibilityWarnings();
|
||||
expect(warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'low-color-tmux',
|
||||
message: expect.stringContaining('Limited color support detected'),
|
||||
priority: WarningPriority.High,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return GNU screen warning when detected', () => {
|
||||
vi.stubEnv('STY', '1234.pts-0.host');
|
||||
|
||||
const warnings = getCompatibilityWarnings();
|
||||
expect(warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'gnu-screen',
|
||||
message: expect.stringContaining('GNU screen detected'),
|
||||
priority: WarningPriority.Low,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return dumb terminal warning when detected', () => {
|
||||
vi.stubEnv('TERM', 'dumb');
|
||||
|
||||
const warnings = getCompatibilityWarnings();
|
||||
expect(warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'dumb-terminal',
|
||||
message: expect.stringContaining('Basic terminal detected'),
|
||||
priority: WarningPriority.High,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return JetBrains warning when detected but NOT in alternate buffer', () => {
|
||||
vi.mocked(os.platform).mockReturnValue('darwin');
|
||||
|
||||
@@ -27,7 +27,60 @@ export function isWindows10(): boolean {
|
||||
* Detects if the current terminal is a JetBrains-based IDE terminal.
|
||||
*/
|
||||
export function isJetBrainsTerminal(): boolean {
|
||||
return process.env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm';
|
||||
const env = process.env;
|
||||
return !!(
|
||||
env['TERMINAL_EMULATOR']?.startsWith('JetBrains') || env['JETBRAINS_IDE']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the current terminal is running inside tmux.
|
||||
*/
|
||||
export function isTmux(): boolean {
|
||||
return !!process.env['TMUX'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the current terminal is running inside GNU screen.
|
||||
*/
|
||||
export function isGnuScreen(): boolean {
|
||||
return !!process.env['STY'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the terminal is low-color mode (TERM=screen* with no COLORTERM).
|
||||
*/
|
||||
export function isLowColorTmux(): boolean {
|
||||
const term = process.env['TERM'] || '';
|
||||
return term.startsWith('screen') && !process.env['COLORTERM'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the terminal is a "dumb" terminal.
|
||||
*/
|
||||
export function isDumbTerminal(): boolean {
|
||||
const term = process.env['TERM'] || '';
|
||||
return term === 'dumb' || term === 'vt100';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the terminal name from environment variables.
|
||||
*/
|
||||
export function getTerminalNameFromEnv(): string {
|
||||
const env = process.env;
|
||||
if (env['TERM_PROGRAM'] && env['TERM_PROGRAM'] !== 'Unknown') {
|
||||
return env['TERM_PROGRAM'];
|
||||
}
|
||||
if (isJetBrainsTerminal()) {
|
||||
return env['JETBRAINS_IDE'] || 'JetBrains IDE';
|
||||
}
|
||||
if (isTmux()) {
|
||||
return 'tmux';
|
||||
}
|
||||
if (isGnuScreen()) {
|
||||
return 'GNU screen';
|
||||
}
|
||||
return env['TERM_PROGRAM'] || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,17 +157,46 @@ export function getCompatibilityWarnings(options?: {
|
||||
}
|
||||
|
||||
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 mouse scrolling is unreliable. Disabling alternate buffer mode in settings or using an external terminal${suggestedTerminals} is recommended.`,
|
||||
message:
|
||||
'⚠️ JetBrains terminal detected — alternate buffer mode may cause scroll wheel issues and rendering artifacts. If you experience problems, disable it in /settings → "Use Alternate Screen Buffer".',
|
||||
priority: WarningPriority.High,
|
||||
});
|
||||
}
|
||||
|
||||
if (isTmux() && options?.isAlternateBuffer) {
|
||||
warnings.push({
|
||||
id: 'tmux-alternate-buffer',
|
||||
message:
|
||||
'⚠️ tmux detected — alternate buffer mode may cause unexpected scrollback loss and flickering. If you experience issues, disable it in /settings → "Use Alternate Screen Buffer".\n Tip: Use Ctrl-b [ to access tmux copy mode for scrolling history.',
|
||||
priority: WarningPriority.High,
|
||||
});
|
||||
}
|
||||
|
||||
if (isLowColorTmux()) {
|
||||
warnings.push({
|
||||
id: 'low-color-tmux',
|
||||
message:
|
||||
'⚠️ Limited color support detected (TERM=screen). Some visual elements may not render correctly. For better color support in tmux, add to ~/.tmux.conf:\n set -g default-terminal "tmux-256color"\n set -ga terminal-overrides ",*256col*:Tc"',
|
||||
priority: WarningPriority.High,
|
||||
});
|
||||
}
|
||||
|
||||
if (isGnuScreen()) {
|
||||
warnings.push({
|
||||
id: 'gnu-screen',
|
||||
message:
|
||||
'⚠️ GNU screen detected. Some keyboard shortcuts and visual features may behave unexpectedly. For the best experience, consider using tmux or running Gemini CLI directly in your terminal.',
|
||||
priority: WarningPriority.Low,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDumbTerminal()) {
|
||||
warnings.push({
|
||||
id: 'dumb-terminal',
|
||||
message:
|
||||
'⚠️ Basic terminal detected (TERM=dumb). Visual rendering will be limited. For the best experience, use a terminal emulator with truecolor support.',
|
||||
priority: WarningPriority.High,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user