From 5f698ac0e51faf4ddf9a6695ac5a8870219df0b6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 3 Mar 2026 21:22:11 +0000 Subject: [PATCH] feat(cli): implement terminal detection and capability-based warnings --- .../utils/terminalCapabilityManager.test.ts | 127 +++++++----------- .../src/ui/utils/terminalCapabilityManager.ts | 30 ++--- packages/core/src/index.ts | 1 + packages/core/src/utils/compatibility.test.ts | 2 +- packages/core/src/utils/compatibility.ts | 66 +++++---- .../src/utils/terminalEnvironment.test.ts | 73 ++++++++++ .../core/src/utils/terminalEnvironment.ts | 84 ++++++++++++ 7 files changed, 255 insertions(+), 128 deletions(-) create mode 100644 packages/core/src/utils/terminalEnvironment.test.ts create mode 100644 packages/core/src/utils/terminalEnvironment.ts diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts index c5c05db38b..3fe3f8c1d0 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts @@ -49,6 +49,7 @@ describe('TerminalCapabilityManager', () => { beforeEach(() => { vi.resetAllMocks(); + vi.useFakeTimers(); // Reset singleton TerminalCapabilityManager.resetInstanceForTesting(); @@ -58,11 +59,9 @@ describe('TerminalCapabilityManager', () => { stdin.isTTY = true; stdin.isRaw = false; stdin.setRawMode = vi.fn(); - stdin.removeListener = vi.fn(); - stdout = { isTTY: true, fd: 1 }; - // Use defineProperty to mock process.stdin/stdout + // Use defineProperty to mock process properties Object.defineProperty(process, 'stdin', { value: stdin, configurable: true, @@ -71,8 +70,6 @@ describe('TerminalCapabilityManager', () => { value: stdout, configurable: true, }); - - vi.useFakeTimers(); }); afterEach(() => { @@ -92,25 +89,24 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate Kitty response: \x1b[?1u + // Simulate kitty protocol response stdin.emit('data', Buffer.from('\x1b[?1u')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + // Simulate sentinel response + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; expect(manager.isKittyProtocolEnabled()).toBe(true); + expect(enableKittyKeyboardProtocol).toHaveBeenCalled(); }); it('should detect Background Color', async () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate OSC 11 response - // \x1b]11;rgb:0000/ff00/0000\x1b\ - // RGB: 0, 255, 0 -> #00ff00 + // Simulate background color response stdin.emit('data', Buffer.from('\x1b]11;rgb:0000/ffff/0000\x1b\\')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + // Simulate sentinel response + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; expect(manager.getTerminalBackgroundColor()).toBe('#00ff00'); @@ -120,10 +116,10 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate Terminal Name response + // Simulate terminal name response stdin.emit('data', Buffer.from('\x1bP>|WezTerm 20240203\x1b\\')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + // Simulate sentinel response + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; expect(manager.getTerminalName()).toBe('WezTerm 20240203'); @@ -133,14 +129,15 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - stdin.emit('data', Buffer.from('\x1b[?1u')); - stdin.emit('data', Buffer.from('\x1b]11;rgb:0000/0000/0000\x1b\\')); - // Sentinel - stdin.emit('data', Buffer.from('\x1b[?62c')); + // Send everything at once + stdin.emit( + 'data', + Buffer.from( + '\x1b[?1u\x1b]11;rgb:0000/0000/0000\x1b\\\x1bP>|xterm\x1b\\\x1b[?1c', + ), + ); - // Should resolve without waiting for timeout await promise; - expect(manager.isKittyProtocolEnabled()).toBe(true); expect(manager.getTerminalBackgroundColor()).toBe('#000000'); }); @@ -149,22 +146,19 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate only Kitty response - stdin.emit('data', Buffer.from('\x1b[?1u')); - - // Advance to timeout + // Don't send any data, just trigger timeout vi.advanceTimersByTime(1000); await promise; - expect(manager.isKittyProtocolEnabled()).toBe(true); + expect(manager.getTerminalBackgroundColor()).toBeUndefined(); }); it('should not detect Kitty if only DA1 (c) is received', async () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate DA1 response only: \x1b[?62;c - stdin.emit('data', Buffer.from('\x1b[?62c')); + // Send only sentinel + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; expect(manager.isKittyProtocolEnabled()).toBe(false); @@ -174,14 +168,13 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Split response: \x1b[? 1u - stdin.emit('data', Buffer.from('\x1b[?')); - stdin.emit('data', Buffer.from('1u')); - // Complete with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + // Split background color response + stdin.emit('data', Buffer.from('\x1b]11;')); + stdin.emit('data', Buffer.from('rgb:ffff/0000/0000\x1b\\')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; - expect(manager.isKittyProtocolEnabled()).toBe(true); + expect(manager.getTerminalBackgroundColor()).toBe('#ff0000'); }); describe('modifyOtherKeys detection', () => { @@ -189,10 +182,9 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate modifyOtherKeys level 2 response: \x1b[>4;2m + // level 2 stdin.emit('data', Buffer.from('\x1b[>4;2m')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; @@ -203,10 +195,9 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate modifyOtherKeys level 0 response: \x1b[>4;0m + // level 0 (disabled) stdin.emit('data', Buffer.from('\x1b[>4;0m')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; @@ -217,14 +208,11 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate both Kitty and modifyOtherKeys responses stdin.emit('data', Buffer.from('\x1b[?1u')); stdin.emit('data', Buffer.from('\x1b[>4;2m')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; - expect(manager.isKittyProtocolEnabled()).toBe(true); expect(enableKittyKeyboardProtocol).toHaveBeenCalled(); expect(enableModifyOtherKeys).not.toHaveBeenCalled(); @@ -234,10 +222,8 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate only modifyOtherKeys response (no Kitty) stdin.emit('data', Buffer.from('\x1b[>4;2m')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; @@ -249,11 +235,9 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Split response: \x1b[>4;2m stdin.emit('data', Buffer.from('\x1b[>4;')); stdin.emit('data', Buffer.from('2m')); - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; @@ -264,17 +248,15 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - stdin.emit('data', Buffer.from('\x1b]11;rgb:1a1a/1a1a/1a1a\x1b\\')); // background color - stdin.emit('data', Buffer.from('\x1bP>|tmux\x1b\\')); // Terminal name - stdin.emit('data', Buffer.from('\x1b[>4;2m')); // modifyOtherKeys - // Complete detection with DA1 - stdin.emit('data', Buffer.from('\x1b[?62c')); + stdin.emit('data', Buffer.from('\x1b]11;rgb:1a1a/1a1a/1a1a\x1b\\')); + stdin.emit('data', Buffer.from('\x1bP>|tmux\x1b\\')); + stdin.emit('data', Buffer.from('\x1b[>4;2m')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; expect(manager.getTerminalBackgroundColor()).toBe('#1a1a1a'); expect(manager.getTerminalName()).toBe('tmux'); - expect(enableModifyOtherKeys).toHaveBeenCalled(); }); @@ -282,8 +264,7 @@ describe('TerminalCapabilityManager', () => { const manager = TerminalCapabilityManager.getInstance(); const promise = manager.detectCapabilities(); - // Simulate only DA1 response (no specific MOK or Kitty response) - stdin.emit('data', Buffer.from('\x1b[?62c')); + stdin.emit('data', Buffer.from('\x1b[?1c')); await promise; @@ -298,18 +279,16 @@ describe('TerminalCapabilityManager', () => { expect(fs.writeSync).toHaveBeenCalledWith( expect.anything(), // eslint-disable-next-line no-control-regex - expect.stringMatching(/^\x1b\[8m.*\x1b\[2K\r\x1b\[0m$/s), + expect.stringMatching(/^\x1b\[8m.*?\x1b\[2K\r\x1b\[0m$/), ); }); }); describe('supportsOsc9Notifications', () => { - const manager = TerminalCapabilityManager.getInstance(); - - it.each([ + const testCases = [ { name: 'WezTerm (terminal name)', - terminalName: 'WezTerm', + terminalName: 'WezTerm 20240203', env: {}, expected: true, }, @@ -327,7 +306,7 @@ describe('TerminalCapabilityManager', () => { }, { name: 'kitty (terminal name)', - terminalName: 'kitty', + terminalName: 'xterm-kitty', env: {}, expected: true, }, @@ -361,18 +340,14 @@ describe('TerminalCapabilityManager', () => { env: { TERM: 'xterm-256color' }, expected: false, }, - { - name: 'Windows Terminal (WT_SESSION)', - terminalName: 'iTerm.app', - env: { WT_SESSION: 'some-guid' }, - expected: false, - }, - ])( - 'should return $expected for $name', - ({ terminalName, env, expected }) => { + ]; + + testCases.forEach(({ name, terminalName, env, expected }) => { + it(`should return ${expected} for '${name}'`, () => { + const manager = TerminalCapabilityManager.getInstance(); vi.spyOn(manager, 'getTerminalName').mockReturnValue(terminalName); expect(manager.supportsOsc9Notifications(env)).toBe(expected); - }, - ); + }); + }); }); }); diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.ts index 27a9b62384..e8c9c37250 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.ts @@ -35,6 +35,9 @@ export function cleanupTerminalOnExit() { disableBracketedPasteMode(); } +/** + * Manages terminal capability detection. + */ export class TerminalCapabilityManager { private static instance: TerminalCapabilityManager | undefined; @@ -47,16 +50,6 @@ export class TerminalCapabilityManager { private static readonly CLEAR_LINE_AND_RETURN = '\x1b[2K\r'; private static readonly RESET_ATTRIBUTES = '\x1b[0m'; - /** - * Triggers a terminal background color query. - * @param stdout The stdout stream to write to. - */ - static queryBackgroundColor(stdout: { - write: (data: string) => void | boolean; - }): void { - stdout.write(TerminalCapabilityManager.OSC_11_QUERY); - } - // Kitty keyboard flags: CSI ? flags u // eslint-disable-next-line no-control-regex private static readonly KITTY_REGEX = /\x1b\[\?(\d+)u/; @@ -80,6 +73,7 @@ export class TerminalCapabilityManager { private kittyEnabled = false; private modifyOtherKeysSupported = false; private terminalName: string | undefined; + private sentinelReceived = false; private constructor() {} @@ -212,6 +206,7 @@ export class TerminalCapabilityManager { ); if (match) { deviceAttributesReceived = true; + this.sentinelReceived = true; cleanup(); } } @@ -270,15 +265,18 @@ export class TerminalCapabilityManager { return this.kittyEnabled; } - isKeyboardProtocolSupported(): boolean { - return this.kittySupported || this.modifyOtherKeysSupported; + /** + * Returns true if keyboard protocol support was explicitly detected. + * Returns false if detection finished and no support was found. + * Returns undefined if detection timed out or failed to receive the sentinel. + */ + isKeyboardProtocolSupported(): boolean | undefined { + if (this.kittySupported || this.modifyOtherKeysSupported) return true; + if (this.sentinelReceived) return false; + return undefined; } supportsOsc9Notifications(env: NodeJS.ProcessEnv = process.env): boolean { - if (env['WT_SESSION']) { - return false; - } - return ( this.hasOsc9TerminalSignature(this.getTerminalName()) || this.hasOsc9TerminalSignature(env['TERM_PROGRAM']) || diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8ce5e77d81..1078bdabaa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -191,6 +191,7 @@ export { logBillingEvent } from './telemetry/loggers.js'; export * from './telemetry/constants.js'; export { sessionId, createSessionId } from './utils/session.js'; export * from './utils/compatibility.js'; +export * from './utils/terminalEnvironment.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 index 5d6c90eedb..87b6cc8eeb 100644 --- a/packages/core/src/utils/compatibility.test.ts +++ b/packages/core/src/utils/compatibility.test.ts @@ -7,7 +7,6 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import os from 'node:os'; import { - isWindows10, isJetBrainsTerminal, supports256Colors, supportsTrueColor, @@ -16,6 +15,7 @@ import { isTmux, supportsKeyboardProtocolHeuristic, } from './compatibility.js'; +import { isWindows10 } from './terminalEnvironment.js'; vi.mock('node:os', () => ({ default: { diff --git a/packages/core/src/utils/compatibility.ts b/packages/core/src/utils/compatibility.ts index c88fd17179..723698c08c 100644 --- a/packages/core/src/utils/compatibility.ts +++ b/packages/core/src/utils/compatibility.ts @@ -5,78 +5,67 @@ */ import os from 'node:os'; +import { + TerminalType, + detectTerminalType, + isWindows10 as detectIsWindows10, +} from './terminalEnvironment.js'; + +// Re-export these for backward compatibility +export { TerminalType, detectTerminalType }; /** * Detects if the current OS is Windows 10. */ -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; -} +// Removed duplicate export to avoid ambiguity in index.ts /** * Detects if the current terminal is a JetBrains-based IDE terminal. */ export function isJetBrainsTerminal(): boolean { - return ( - process.env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm' || - process.env['TERM_PROGRAM'] === 'JetBrains-JediTerm' || - !!process.env['IDEA_INITIAL_DIRECTORY'] || - !!process.env['JETBRAINS_IDE'] - ); + return detectTerminalType() === TerminalType.JetBrains; } /** * Detects if the current terminal is the default Apple Terminal.app. */ export function isAppleTerminal(): boolean { - return process.env['TERM_PROGRAM'] === 'Apple_Terminal'; + return detectTerminalType() === TerminalType.AppleTerminal; } /** * Detects if the current terminal is VS Code. */ export function isVSCode(): boolean { - return process.env['TERM_PROGRAM'] === 'vscode'; + return detectTerminalType() === TerminalType.VSCode; } /** * Detects if the current terminal is iTerm2. */ export function isITerm2(): boolean { - return process.env['TERM_PROGRAM'] === 'iTerm.app'; + return detectTerminalType() === TerminalType.ITerm2; } /** * Detects if the current terminal is Ghostty. */ export function isGhostty(): boolean { - return ( - process.env['TERM_PROGRAM'] === 'ghostty' || - !!process.env['GHOSTTY_BIN_DIR'] - ); + return detectTerminalType() === TerminalType.Ghostty; } /** * Detects if running inside tmux. */ export function isTmux(): boolean { - return !!process.env['TMUX'] || (process.env['TERM'] || '').includes('tmux'); + return detectTerminalType() === TerminalType.Tmux; } /** * Detects if the current terminal is Windows Terminal. */ export function isWindowsTerminal(): boolean { - return !!process.env['WT_SESSION']; + return detectTerminalType() === TerminalType.WindowsTerminal; } /** @@ -121,7 +110,13 @@ export function supportsTrueColor(): boolean { * Heuristic for keyboard protocol support based on terminal identity. */ export function supportsKeyboardProtocolHeuristic(): boolean { - return isGhostty() || isITerm2() || isVSCode() || isWindowsTerminal(); + const type = detectTerminalType(); + return ( + type === TerminalType.Ghostty || + type === TerminalType.ITerm2 || + type === TerminalType.VSCode || + type === TerminalType.WindowsTerminal + ); } export enum WarningPriority { @@ -143,8 +138,9 @@ export function getCompatibilityWarnings(options?: { supportsKeyboardProtocol?: boolean; }): StartupWarning[] { const warnings: StartupWarning[] = []; + const type = detectTerminalType(); - if (isWindows10()) { + if (detectIsWindows10()) { warnings.push({ id: 'windows-10', message: @@ -153,7 +149,7 @@ export function getCompatibilityWarnings(options?: { }); } - if (isJetBrainsTerminal() && options?.isAlternateBuffer) { + if (type === TerminalType.JetBrains && options?.isAlternateBuffer) { const platformTerminals: Partial> = { win32: 'Windows Terminal', darwin: 'iTerm2 or Ghostty', @@ -169,7 +165,7 @@ export function getCompatibilityWarnings(options?: { }); } - if (isTmux()) { + if (type === TerminalType.Tmux) { warnings.push({ id: 'tmux-mouse-support', message: @@ -187,10 +183,10 @@ export function getCompatibilityWarnings(options?: { }); } else if ( !supportsTrueColor() && - !isITerm2() && - !isVSCode() && - !isGhostty() && - !isAppleTerminal() + type !== TerminalType.ITerm2 && + type !== TerminalType.VSCode && + type !== TerminalType.Ghostty && + type !== TerminalType.AppleTerminal ) { warnings.push({ id: 'true-color', diff --git a/packages/core/src/utils/terminalEnvironment.test.ts b/packages/core/src/utils/terminalEnvironment.test.ts new file mode 100644 index 0000000000..316260fefc --- /dev/null +++ b/packages/core/src/utils/terminalEnvironment.test.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { TerminalType, detectTerminalType } from './terminalEnvironment.js'; + +describe('terminalEnvironment', () => { + describe('detectTerminalType', () => { + it('should detect JetBrains', () => { + expect( + detectTerminalType({ TERMINAL_EMULATOR: 'JetBrains-JediTerm' }), + ).toBe(TerminalType.JetBrains); + expect(detectTerminalType({ IDEA_INITIAL_DIRECTORY: '/some/path' })).toBe( + TerminalType.JetBrains, + ); + }); + + it('should detect tmux', () => { + expect(detectTerminalType({ TMUX: '/tmp/tmux-1000/default,123,0' })).toBe( + TerminalType.Tmux, + ); + expect(detectTerminalType({ TERM: 'screen-256color' })).not.toBe( + TerminalType.Tmux, + ); + expect(detectTerminalType({ TERM: 'tmux-256color' })).toBe( + TerminalType.Tmux, + ); + }); + + it('should detect VSCode', () => { + expect(detectTerminalType({ TERM_PROGRAM: 'vscode' })).toBe( + TerminalType.VSCode, + ); + expect(detectTerminalType({ VSCODE_GIT_IPC_HANDLE: 'something' })).toBe( + TerminalType.VSCode, + ); + }); + + it('should detect iTerm2', () => { + expect(detectTerminalType({ TERM_PROGRAM: 'iTerm.app' })).toBe( + TerminalType.ITerm2, + ); + }); + + it('should detect Ghostty', () => { + expect(detectTerminalType({ TERM_PROGRAM: 'ghostty' })).toBe( + TerminalType.Ghostty, + ); + expect(detectTerminalType({ GHOSTTY_BIN_DIR: '/usr/bin' })).toBe( + TerminalType.Ghostty, + ); + }); + + it('should detect Windows Terminal', () => { + expect(detectTerminalType({ WT_SESSION: 'guid' })).toBe( + TerminalType.WindowsTerminal, + ); + }); + + it('should fallback to xterm', () => { + expect(detectTerminalType({ TERM: 'xterm-256color' })).toBe( + TerminalType.XTerm, + ); + }); + + it('should return Unknown for unknown environments', () => { + expect(detectTerminalType({})).toBe(TerminalType.Unknown); + }); + }); +}); diff --git a/packages/core/src/utils/terminalEnvironment.ts b/packages/core/src/utils/terminalEnvironment.ts new file mode 100644 index 0000000000..ac71b74b13 --- /dev/null +++ b/packages/core/src/utils/terminalEnvironment.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import os from 'node:os'; + +/** + * Known terminal types that Gemini CLI recognizes for specialized behavior. + */ +export enum TerminalType { + Unknown = 'unknown', + JetBrains = 'jetbrains', + Tmux = 'tmux', + VSCode = 'vscode', + ITerm2 = 'iterm2', + Ghostty = 'ghostty', + AppleTerminal = 'apple_terminal', + WindowsTerminal = 'windows_terminal', + XTerm = 'xterm', +} + +/** + * Detects the current terminal type based on environment variables. + */ +export function detectTerminalType( + env: NodeJS.ProcessEnv = process.env, +): TerminalType { + if ( + env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm' || + env['TERM_PROGRAM'] === 'JetBrains-JediTerm' || + !!env['IDEA_INITIAL_DIRECTORY'] || + !!env['JETBRAINS_IDE'] + ) { + return TerminalType.JetBrains; + } + + if (!!env['TMUX'] || (env['TERM'] || '').includes('tmux')) { + return TerminalType.Tmux; + } + + if (env['TERM_PROGRAM'] === 'vscode' || !!env['VSCODE_GIT_IPC_HANDLE']) { + return TerminalType.VSCode; + } + + if (env['TERM_PROGRAM'] === 'iTerm.app') { + return TerminalType.ITerm2; + } + + if (env['TERM_PROGRAM'] === 'ghostty' || !!env['GHOSTTY_BIN_DIR']) { + return TerminalType.Ghostty; + } + + if (env['TERM_PROGRAM'] === 'Apple_Terminal') { + return TerminalType.AppleTerminal; + } + + if (env['WT_SESSION']) { + return TerminalType.WindowsTerminal; + } + + if ((env['TERM'] || '').includes('xterm')) { + return TerminalType.XTerm; + } + + return TerminalType.Unknown; +} + +/** + * Detects if the current OS is Windows 10. + */ +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; +}