diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index ee2c18d1a2..e048179a1d 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -110,6 +110,7 @@ import { isWorkspaceTrusted } from '../config/trustedFolders.js'; import { disableMouseEvents, enableMouseEvents } from './utils/mouse.js'; import { useAlternateBuffer } from './hooks/useAlternateBuffer.js'; import { useSettings } from './contexts/SettingsContext.js'; +import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js'; const WARNING_PROMPT_DURATION_MS = 1000; const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000; @@ -381,6 +382,11 @@ export const AppContainer = (props: AppContainerProps) => { setHistoryRemountKey((prev) => prev + 1); }, [setHistoryRemountKey, stdout, isAlternateBuffer]); + const handleEditorClose = useCallback(() => { + enableSupportedProtocol(); + refreshStatic(); + }, [refreshStatic]); + const { isThemeDialogOpen, openThemeDialog, @@ -687,7 +693,7 @@ Logging in with Google... Please restart Gemini CLI to continue. performMemoryRefresh, modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError, - refreshStatic, + handleEditorClose, onCancelSubmit, setEmbeddedShellFocused, terminalWidth, diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 4313d0ced6..060c9f7475 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -207,7 +207,6 @@ describe('InputPrompt', () => { ); mockedUseKittyKeyboardProtocol.mockReturnValue({ - supported: false, enabled: false, checking: false, }); @@ -1244,7 +1243,6 @@ describe('InputPrompt', () => { beforeEach(() => { vi.useFakeTimers(); mockedUseKittyKeyboardProtocol.mockReturnValue({ - supported: false, enabled: false, checking: false, }); @@ -1328,7 +1326,6 @@ describe('InputPrompt', () => { name: 'kitty', setup: () => mockedUseKittyKeyboardProtocol.mockReturnValue({ - supported: true, enabled: true, checking: false, }), diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index cc5af33d61..073cd1c16a 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -404,7 +404,7 @@ export const InputPrompt: React.FC = ({ if (key.paste) { // Record paste time to prevent accidental auto-submission - if (!isTerminalPasteTrusted(kittyProtocol.supported)) { + if (!isTerminalPasteTrusted(kittyProtocol.enabled)) { setRecentUnsafePasteTime(Date.now()); // Clear any existing paste timeout @@ -820,7 +820,7 @@ export const InputPrompt: React.FC = ({ recentUnsafePasteTime, commandSearchActive, commandSearchCompletion, - kittyProtocol.supported, + kittyProtocol.enabled, tryLoadQueuedMessages, setBannerVisible, ], diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index d711a57477..04366c14a3 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -20,6 +20,7 @@ import { import type { Key } from '../../contexts/KeypressContext.js'; import type { VimAction } from './vim-buffer-actions.js'; import { handleVimAction } from './vim-buffer-actions.js'; +import { enableSupportedProtocol } from '../../utils/kittyProtocolDetector.js'; export type Direction = | 'left' @@ -1891,6 +1892,7 @@ export function useTextBuffer({ } catch (err) { console.error('[useTextBuffer] external editor error', err); } finally { + enableSupportedProtocol(); if (wasRaw) setRawMode?.(true); try { fs.unlinkSync(filePath); diff --git a/packages/cli/src/ui/hooks/useKittyKeyboardProtocol.ts b/packages/cli/src/ui/hooks/useKittyKeyboardProtocol.ts index 53c7566c69..4a89b165b2 100644 --- a/packages/cli/src/ui/hooks/useKittyKeyboardProtocol.ts +++ b/packages/cli/src/ui/hooks/useKittyKeyboardProtocol.ts @@ -5,13 +5,9 @@ */ import { useState } from 'react'; -import { - isKittyProtocolEnabled, - isKittyProtocolSupported, -} from '../utils/kittyProtocolDetector.js'; +import { isKittyProtocolEnabled } from '../utils/kittyProtocolDetector.js'; export interface KittyProtocolStatus { - supported: boolean; enabled: boolean; checking: boolean; } @@ -22,7 +18,6 @@ export interface KittyProtocolStatus { */ export function useKittyKeyboardProtocol(): KittyProtocolStatus { const [status] = useState({ - supported: isKittyProtocolSupported(), enabled: isKittyProtocolEnabled(), checking: false, }); diff --git a/packages/cli/src/ui/utils/kittyProtocolDetector.ts b/packages/cli/src/ui/utils/kittyProtocolDetector.ts index 2d3e7a9d70..0dc1d02ace 100644 --- a/packages/cli/src/ui/utils/kittyProtocolDetector.ts +++ b/packages/cli/src/ui/utils/kittyProtocolDetector.ts @@ -5,8 +5,11 @@ */ let detectionComplete = false; -let protocolSupported = false; -let protocolEnabled = false; + +let kittySupported = false; +let sgrMouseSupported = false; + +let kittyEnabled = false; let sgrMouseEnabled = false; /** @@ -14,15 +17,15 @@ let sgrMouseEnabled = false; * Definitive document about this protocol lives at https://sw.kovidgoyal.net/kitty/keyboard-protocol/ * This function should be called once at app startup. */ -export async function detectAndEnableKittyProtocol(): Promise { +export async function detectAndEnableKittyProtocol(): Promise { if (detectionComplete) { - return protocolSupported; + return; } return new Promise((resolve) => { if (!process.stdin.isTTY || !process.stdout.isTTY) { detectionComplete = true; - resolve(false); + resolve(); return; } @@ -35,14 +38,24 @@ export async function detectAndEnableKittyProtocol(): Promise { let progressiveEnhancementReceived = false; let timeoutId: NodeJS.Timeout | undefined; - const onTimeout = () => { - timeoutId = undefined; + const finish = () => { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + timeoutId = undefined; + } process.stdin.removeListener('data', handleData); if (!originalRawMode) { process.stdin.setRawMode(false); } + + if (kittySupported || sgrMouseSupported) { + enableSupportedProtocol(); + process.on('exit', disableAllProtocols); + process.on('SIGTERM', disableAllProtocols); + } + detectionComplete = true; - resolve(false); + resolve(); }; const handleData = (data: Buffer) => { @@ -59,37 +72,20 @@ export async function detectAndEnableKittyProtocol(): Promise { // indication the terminal probably supports kitty and we just need to // wait a bit longer for a response. clearTimeout(timeoutId); - timeoutId = setTimeout(onTimeout, 1000); + timeoutId = setTimeout(finish, 1000); } // Check for device attributes response (CSI ? c) if (responseBuffer.includes('\x1b[?') && responseBuffer.includes('c')) { - clearTimeout(timeoutId); - timeoutId = undefined; - process.stdin.removeListener('data', handleData); - - if (!originalRawMode) { - process.stdin.setRawMode(false); - } - if (progressiveEnhancementReceived) { - // Enable the protocol - process.stdout.write('\x1b[>1u'); - protocolSupported = true; - protocolEnabled = true; + kittySupported = true; } // Broaden mouse support by enabling SGR mode if we get any device // attribute response, which is a strong signal of a modern terminal. - process.stdout.write('\x1b[?1006h'); - sgrMouseEnabled = true; + sgrMouseSupported = true; - // Set up cleanup on exit for all enabled protocols - process.on('exit', disableAllProtocols); - process.on('SIGTERM', disableAllProtocols); - - detectionComplete = true; - resolve(protocolSupported); + finish(); } }; @@ -102,14 +98,18 @@ export async function detectAndEnableKittyProtocol(): Promise { // Timeout after 200ms // When a iterm2 terminal does not have focus this can take over 90s on a // fast macbook so we need a somewhat longer threshold than would be ideal. - timeoutId = setTimeout(onTimeout, 200); + timeoutId = setTimeout(finish, 200); }); } +export function isKittyProtocolEnabled(): boolean { + return kittyEnabled; +} + function disableAllProtocols() { - if (protocolEnabled) { + if (kittyEnabled) { process.stdout.write('\x1b[1u'); + kittyEnabled = true; + } + if (sgrMouseSupported) { + process.stdout.write('\x1b[?1006h'); + sgrMouseEnabled = true; + } }