diff --git a/packages/core/src/utils/compatibility.test.ts b/packages/core/src/utils/compatibility.test.ts index faf0dd579d..23597e0c7a 100644 --- a/packages/core/src/utils/compatibility.test.ts +++ b/packages/core/src/utils/compatibility.test.ts @@ -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; + 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'); diff --git a/packages/core/src/utils/compatibility.ts b/packages/core/src/utils/compatibility.ts index 15b2ae24b4..0b902c064d 100644 --- a/packages/core/src/utils/compatibility.ts +++ b/packages/core/src/utils/compatibility.ts @@ -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> = { - 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, }); }