From 8e313539abc325d3b9bc8a0ef17961a2d8686e3d Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 9 Jan 2026 15:05:15 +0530 Subject: [PATCH 1/8] add one time prompt for /terminal-setup Co-authored-by: Vedant Mahajan --- packages/cli/src/ui/AppContainer.tsx | 66 ++++++++++++++ .../cli/src/ui/utils/terminalSetup.test.ts | 54 +++++++++++- packages/cli/src/ui/utils/terminalSetup.ts | 86 +++++++++++++++++++ packages/cli/src/utils/persistentState.ts | 1 + 4 files changed, 206 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 7687a096c14..70214335df3 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -123,6 +123,11 @@ import { useSettings } from './contexts/SettingsContext.js'; import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'; import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; import { useBanner } from './hooks/useBanner.js'; +import { persistentState } from '../utils/persistentState.js'; +import { + shouldPromptForTerminalSetup, + terminalSetup, +} from './utils/terminalSetup.js'; import { useHookDisplayState } from './hooks/useHookDisplayState.js'; import { WARNING_PROMPT_DURATION_MS, @@ -419,6 +424,67 @@ export const AppContainer = (props: AppContainerProps) => { initializeFromLogger(logger); }, [logger, initializeFromLogger]); + // One-time prompt to suggest running /terminal-setup when it would help. + useEffect(() => { + if ( + !isConfigInitialized || + !config.isInteractive() || + !process.stdin.isTTY + ) { + return; + } + + const hasBeenPrompted = persistentState.get('terminalSetupPromptShown'); + if (hasBeenPrompted) { + return; + } + + let cancelled = false; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + (async () => { + const shouldPrompt = await shouldPromptForTerminalSetup(); + if (!shouldPrompt || cancelled) return; + + // Record that we've shown the prompt so it never appears again. + persistentState.set('terminalSetupPromptShown', true); + + const consentText = + 'Would you like to run `/terminal-setup` to configure Shift+Enter and Ctrl+Enter for multiline input in this terminal?'; + + const confirmed = await requestConsentInteractive( + consentText, + addConfirmUpdateExtensionRequest, + ); + + if (!confirmed || cancelled) return; + + const result = await terminalSetup(); + if (cancelled) return; + + historyManager.addItem( + { + type: result.success ? 'info' : 'error', + text: + result.message + + (result.success && result.requiresRestart + ? '\n\nPlease restart your terminal for the changes to take effect.' + : ''), + }, + Date.now(), + ); + })(); + + return () => { + cancelled = true; + }; + }, [ + isConfigInitialized, + config, + addConfirmUpdateExtensionRequest, + historyManager, + ]); + const refreshStatic = useCallback(() => { if (!isAlternateBuffer) { stdout.write(ansiEscapes.clearTerminal); diff --git a/packages/cli/src/ui/utils/terminalSetup.test.ts b/packages/cli/src/ui/utils/terminalSetup.test.ts index a6f7b290a76..b4d3de83fab 100644 --- a/packages/cli/src/ui/utils/terminalSetup.test.ts +++ b/packages/cli/src/ui/utils/terminalSetup.test.ts @@ -5,7 +5,12 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { terminalSetup, VSCODE_SHIFT_ENTER_SEQUENCE } from './terminalSetup.js'; +import { + terminalSetup, + VSCODE_SHIFT_ENTER_SEQUENCE, + shouldPromptForTerminalSetup, +} from './terminalSetup.js'; +import { terminalCapabilityManager } from './terminalCapabilityManager.js'; // Mock dependencies const mocks = vi.hoisted(() => ({ @@ -174,4 +179,51 @@ describe('terminalSetup', () => { expect(mocks.writeFile).toHaveBeenCalled(); }); }); + + describe('shouldPromptForTerminalSetup', () => { + it('should return false when kitty protocol is already enabled', async () => { + vi.mocked( + terminalCapabilityManager.isKittyProtocolEnabled, + ).mockReturnValue(true); + + const result = await shouldPromptForTerminalSetup(); + expect(result).toBe(false); + }); + + it('should return false when both Shift+Enter and Ctrl+Enter bindings already exist', async () => { + vi.mocked( + terminalCapabilityManager.isKittyProtocolEnabled, + ).mockReturnValue(false); + process.env['TERM_PROGRAM'] = 'vscode'; + + const existingBindings = [ + { + key: 'shift+enter', + command: 'workbench.action.terminal.sendSequence', + args: { text: VSCODE_SHIFT_ENTER_SEQUENCE }, + }, + { + key: 'ctrl+enter', + command: 'workbench.action.terminal.sendSequence', + args: { text: VSCODE_SHIFT_ENTER_SEQUENCE }, + }, + ]; + mocks.readFile.mockResolvedValue(JSON.stringify(existingBindings)); + + const result = await shouldPromptForTerminalSetup(); + expect(result).toBe(false); + }); + + it('should return true when keybindings file does not exist', async () => { + vi.mocked( + terminalCapabilityManager.isKittyProtocolEnabled, + ).mockReturnValue(false); + process.env['TERM_PROGRAM'] = 'vscode'; + + mocks.readFile.mockRejectedValue(new Error('ENOENT')); + + const result = await shouldPromptForTerminalSetup(); + expect(result).toBe(true); + }); + }); }); diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index ede409dd495..512b8cc57d0 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -320,6 +320,92 @@ async function configureAntigravity(): Promise { return configureVSCodeStyle('Antigravity', 'Antigravity'); } +/** + * Determines whether it is useful to prompt the user to run /terminal-setup + * in the current environment. + * + * Returns true when: + * - Kitty/modifyOtherKeys keyboard protocol is not already enabled, and + * - We're running inside a supported terminal (VS Code, Cursor, Windsurf, Antigravity), and + * - The keybindings file either does not exist or does not already contain both + * of our Shift+Enter and Ctrl+Enter bindings. + */ +export async function shouldPromptForTerminalSetup(): Promise { + if (terminalCapabilityManager.isKittyProtocolEnabled()) { + return false; + } + + const terminal = await detectTerminal(); + if (!terminal) { + return false; + } + + let appName: string; + switch (terminal) { + case 'vscode': + appName = 'Code'; + break; + case 'cursor': + appName = 'Cursor'; + break; + case 'windsurf': + appName = 'Windsurf'; + break; + case 'antigravity': + appName = 'Antigravity'; + break; + default: + return false; + } + + const configDir = getVSCodeStyleConfigDir(appName); + if (!configDir) { + return false; + } + + const keybindingsFile = path.join(configDir, 'keybindings.json'); + + try { + const content = await fs.readFile(keybindingsFile, 'utf8'); + const cleanContent = stripJsonComments(content); + const parsedContent = JSON.parse(cleanContent); + + if (!Array.isArray(parsedContent)) { + return true; + } + + const hasOurShiftEnter = parsedContent.some((kb) => { + const binding = kb as { + command?: string; + args?: { text?: string }; + key?: string; + }; + return ( + binding.key === 'shift+enter' && + binding.command === 'workbench.action.terminal.sendSequence' && + binding.args?.text === VSCODE_SHIFT_ENTER_SEQUENCE + ); + }); + + const hasOurCtrlEnter = parsedContent.some((kb) => { + const binding = kb as { + command?: string; + args?: { text?: string }; + key?: string; + }; + return ( + binding.key === 'ctrl+enter' && + binding.command === 'workbench.action.terminal.sendSequence' && + binding.args?.text === VSCODE_SHIFT_ENTER_SEQUENCE + ); + }); + + return !(hasOurShiftEnter && hasOurCtrlEnter); + } catch { + return true; + } +} + /** * Main terminal setup function that detects and configures the current terminal. * diff --git a/packages/cli/src/utils/persistentState.ts b/packages/cli/src/utils/persistentState.ts index f5fc4d4b29d..3e33ab81ee5 100644 --- a/packages/cli/src/utils/persistentState.ts +++ b/packages/cli/src/utils/persistentState.ts @@ -12,6 +12,7 @@ const STATE_FILENAME = 'state.json'; interface PersistentStateData { defaultBannerShownCount?: Record; + terminalSetupPromptShown?: boolean; // Add other persistent state keys here as needed } From b382abc6c76053c74abd5cbeec9a8d1c052ca34e Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 9 Jan 2026 15:54:32 +0530 Subject: [PATCH 2/8] improve catch block --- packages/cli/src/ui/utils/terminalSetup.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index 512b8cc57d0..39ea789eaa1 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -401,7 +401,10 @@ export async function shouldPromptForTerminalSetup(): Promise { }); return !(hasOurShiftEnter && hasOurCtrlEnter); - } catch { + } catch (error) { + debugLogger.debug( + `Failed to read or parse keybindings, assuming prompt is needed: ${error}`, + ); return true; } } From c4e1c2ecc173e9a84f80d2f532aeb1a6ad911092 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Wed, 14 Jan 2026 18:28:03 +0530 Subject: [PATCH 3/8] refactor and add gemini comments --- packages/cli/src/ui/AppContainer.tsx | 65 +------ .../src/ui/hooks/useTerminalSetupPrompt.ts | 76 ++++++++ packages/cli/src/ui/utils/terminalSetup.ts | 174 ++++++++---------- 3 files changed, 154 insertions(+), 161 deletions(-) create mode 100644 packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index c09d1bfa9a7..a64440e0622 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -121,11 +121,7 @@ import { useSettings } from './contexts/SettingsContext.js'; import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'; import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; import { useBanner } from './hooks/useBanner.js'; -import { persistentState } from '../utils/persistentState.js'; -import { - shouldPromptForTerminalSetup, - terminalSetup, -} from './utils/terminalSetup.js'; +import { useTerminalSetupPrompt } from './hooks/useTerminalSetupPrompt.js'; import { useHookDisplayState } from './hooks/useHookDisplayState.js'; import { WARNING_PROMPT_DURATION_MS, @@ -418,65 +414,10 @@ export const AppContainer = (props: AppContainerProps) => { }, [logger, initializeFromLogger]); // One-time prompt to suggest running /terminal-setup when it would help. - useEffect(() => { - if ( - !isConfigInitialized || - !config.isInteractive() || - !process.stdin.isTTY - ) { - return; - } - - const hasBeenPrompted = persistentState.get('terminalSetupPromptShown'); - if (hasBeenPrompted) { - return; - } - - let cancelled = false; - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - (async () => { - const shouldPrompt = await shouldPromptForTerminalSetup(); - if (!shouldPrompt || cancelled) return; - - // Record that we've shown the prompt so it never appears again. - persistentState.set('terminalSetupPromptShown', true); - - const consentText = - 'Would you like to run `/terminal-setup` to configure Shift+Enter and Ctrl+Enter for multiline input in this terminal?'; - - const confirmed = await requestConsentInteractive( - consentText, - addConfirmUpdateExtensionRequest, - ); - - if (!confirmed || cancelled) return; - - const result = await terminalSetup(); - if (cancelled) return; - - historyManager.addItem( - { - type: result.success ? 'info' : 'error', - text: - result.message + - (result.success && result.requiresRestart - ? '\n\nPlease restart your terminal for the changes to take effect.' - : ''), - }, - Date.now(), - ); - })(); - - return () => { - cancelled = true; - }; - }, [ - isConfigInitialized, - config, + useTerminalSetupPrompt({ addConfirmUpdateExtensionRequest, historyManager, - ]); + }); const refreshStatic = useCallback(() => { if (!isAlternateBuffer) { diff --git a/packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts b/packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts new file mode 100644 index 00000000000..c128ce688ab --- /dev/null +++ b/packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect } from 'react'; +import { persistentState } from '../../utils/persistentState.js'; +import { + shouldPromptForTerminalSetup, + terminalSetup, + formatTerminalSetupResultMessage, + TERMINAL_SETUP_CONSENT_MESSAGE, +} from '../utils/terminalSetup.js'; +import { requestConsentInteractive } from '../../config/extensions/consent.js'; +import type { ConfirmationRequest } from '../types.js'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; + +interface UseTerminalSetupPromptParams { + /** Function to add a confirmation request to the UI */ + addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void; + /** History manager to display result messages */ + historyManager: UseHistoryManagerReturn; +} + +/** + * Hook that shows a one-time prompt to run /terminal-setup when it would help. + * + * The prompt is shown only once per user (tracked via persistentState) and only + * when shouldPromptForTerminalSetup() returns true (i.e., in a supported terminal + * without the necessary keybindings configured). + */ +export function useTerminalSetupPrompt({ + addConfirmUpdateExtensionRequest, + historyManager, +}: UseTerminalSetupPromptParams): void { + useEffect(() => { + const hasBeenPrompted = persistentState.get('terminalSetupPromptShown'); + if (hasBeenPrompted) { + return; + } + + let cancelled = false; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + (async () => { + const shouldPrompt = await shouldPromptForTerminalSetup(); + if (!shouldPrompt || cancelled) return; + + // Record that we've shown the prompt so it never appears again. + persistentState.set('terminalSetupPromptShown', true); + + const confirmed = await requestConsentInteractive( + TERMINAL_SETUP_CONSENT_MESSAGE, + addConfirmUpdateExtensionRequest, + ); + + if (!confirmed || cancelled) return; + + const result = await terminalSetup(); + if (cancelled) return; + + historyManager.addItem( + { + type: result.success ? 'info' : 'error', + text: formatTerminalSetupResultMessage(result), + }, + Date.now(), + ); + })(); + + return () => { + cancelled = true; + }; + }, [addConfirmUpdateExtensionRequest, historyManager]); +} diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index 39ea789eaa1..4c292dff139 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -54,6 +54,65 @@ export interface TerminalSetupResult { type SupportedTerminal = 'vscode' | 'cursor' | 'windsurf' | 'antigravity'; +/** + * Terminal metadata used for configuration. + */ +interface TerminalData { + /** Display name for the terminal (e.g., "VS Code") */ + terminalName: string; + /** Application folder name used in config paths (e.g., "Code") */ + appName: string; +} + +/** + * Maps a supported terminal ID to its display name and config folder name. + * Returns null if the terminal is not supported. + */ +function getTerminalData(terminal: SupportedTerminal): TerminalData | null { + switch (terminal) { + case 'vscode': + return { terminalName: 'VS Code', appName: 'Code' }; + case 'cursor': + return { terminalName: 'Cursor', appName: 'Cursor' }; + case 'windsurf': + return { terminalName: 'Windsurf', appName: 'Windsurf' }; + case 'antigravity': + return { terminalName: 'Antigravity', appName: 'Antigravity' }; + default: + return null; + } +} + +/** + * Type for a keybinding entry in keybindings.json + */ +type Keybinding = { + key?: string; + command?: string; + args?: { text?: string }; +}; + +/** + * Checks if a keybindings array contains our specific binding for a given key. + * Our bindings are identified by: + * - The key ('shift+enter' or 'ctrl+enter') + * - The command 'workbench.action.terminal.sendSequence' + * - The args.text value matching VSCODE_SHIFT_ENTER_SEQUENCE + */ +function hasOurBinding( + keybindings: unknown[], + key: 'shift+enter' | 'ctrl+enter', +): boolean { + return keybindings.some((kb) => { + const binding = kb as Keybinding; + return ( + binding.key === key && + binding.command === 'workbench.action.terminal.sendSequence' && + binding.args?.text === VSCODE_SHIFT_ENTER_SEQUENCE + ); + }); +} + export function getTerminalProgram(): SupportedTerminal | null { const termProgram = process.env['TERM_PROGRAM']; @@ -219,31 +278,8 @@ async function configureVSCodeStyle( }; // Check if our specific bindings already exist - const hasOurShiftEnter = keybindings.some((kb) => { - const binding = kb as { - command?: string; - args?: { text?: string }; - key?: string; - }; - return ( - binding.key === 'shift+enter' && - binding.command === 'workbench.action.terminal.sendSequence' && - binding.args?.text === '\\\r\n' - ); - }); - - const hasOurCtrlEnter = keybindings.some((kb) => { - const binding = kb as { - command?: string; - args?: { text?: string }; - key?: string; - }; - return ( - binding.key === 'ctrl+enter' && - binding.command === 'workbench.action.terminal.sendSequence' && - binding.args?.text === '\\\r\n' - ); - }); + const hasOurShiftEnter = hasOurBinding(keybindings, 'shift+enter'); + const hasOurCtrlEnter = hasOurBinding(keybindings, 'ctrl+enter'); if (hasOurShiftEnter && hasOurCtrlEnter) { return { @@ -302,24 +338,6 @@ async function configureVSCodeStyle( } } -// Terminal-specific configuration functions - -async function configureVSCode(): Promise { - return configureVSCodeStyle('VS Code', 'Code'); -} - -async function configureCursor(): Promise { - return configureVSCodeStyle('Cursor', 'Cursor'); -} - -async function configureWindsurf(): Promise { - return configureVSCodeStyle('Windsurf', 'Windsurf'); -} - -async function configureAntigravity(): Promise { - return configureVSCodeStyle('Antigravity', 'Antigravity'); -} - /** * Determines whether it is useful to prompt the user to run /terminal-setup * in the current environment. @@ -340,25 +358,12 @@ export async function shouldPromptForTerminalSetup(): Promise { return false; } - let appName: string; - switch (terminal) { - case 'vscode': - appName = 'Code'; - break; - case 'cursor': - appName = 'Cursor'; - break; - case 'windsurf': - appName = 'Windsurf'; - break; - case 'antigravity': - appName = 'Antigravity'; - break; - default: - return false; + const terminalData = getTerminalData(terminal); + if (!terminalData) { + return false; } - const configDir = getVSCodeStyleConfigDir(appName); + const configDir = getVSCodeStyleConfigDir(terminalData.appName); if (!configDir) { return false; } @@ -374,31 +379,8 @@ export async function shouldPromptForTerminalSetup(): Promise { return true; } - const hasOurShiftEnter = parsedContent.some((kb) => { - const binding = kb as { - command?: string; - args?: { text?: string }; - key?: string; - }; - return ( - binding.key === 'shift+enter' && - binding.command === 'workbench.action.terminal.sendSequence' && - binding.args?.text === VSCODE_SHIFT_ENTER_SEQUENCE - ); - }); - - const hasOurCtrlEnter = parsedContent.some((kb) => { - const binding = kb as { - command?: string; - args?: { text?: string }; - key?: string; - }; - return ( - binding.key === 'ctrl+enter' && - binding.command === 'workbench.action.terminal.sendSequence' && - binding.args?.text === VSCODE_SHIFT_ENTER_SEQUENCE - ); - }); + const hasOurShiftEnter = hasOurBinding(parsedContent, 'shift+enter'); + const hasOurCtrlEnter = hasOurBinding(parsedContent, 'ctrl+enter'); return !(hasOurShiftEnter && hasOurCtrlEnter); } catch (error) { @@ -448,19 +430,13 @@ export async function terminalSetup(): Promise { }; } - switch (terminal) { - case 'vscode': - return configureVSCode(); - case 'cursor': - return configureCursor(); - case 'windsurf': - return configureWindsurf(); - case 'antigravity': - return configureAntigravity(); - default: - return { - success: false, - message: `Terminal "${terminal}" is not supported yet.`, - }; + const terminalData = getTerminalData(terminal); + if (!terminalData) { + return { + success: false, + message: `Terminal "${terminal}" is not supported yet.`, + }; } + + return configureVSCodeStyle(terminalData.terminalName, terminalData.appName); } From bd9533b33a0217b659def73a761ec6380e193d7f Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 16 Jan 2026 20:36:04 +0530 Subject: [PATCH 4/8] refactor and include suggestions --- packages/cli/src/ui/AppContainer.tsx | 2 +- .../src/ui/hooks/useTerminalSetupPrompt.ts | 76 -------------- packages/cli/src/ui/utils/terminalSetup.ts | 98 +++++++++++++++---- 3 files changed, 82 insertions(+), 94 deletions(-) delete mode 100644 packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index d71ddb81d0c..56477f0bdad 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -121,7 +121,7 @@ import { useSettings } from './contexts/SettingsContext.js'; import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'; import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; import { useBanner } from './hooks/useBanner.js'; -import { useTerminalSetupPrompt } from './hooks/useTerminalSetupPrompt.js'; +import { useTerminalSetupPrompt } from './utils/terminalSetup.js'; import { useHookDisplayState } from './hooks/useHookDisplayState.js'; import { WARNING_PROMPT_DURATION_MS, diff --git a/packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts b/packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts deleted file mode 100644 index c128ce688ab..00000000000 --- a/packages/cli/src/ui/hooks/useTerminalSetupPrompt.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect } from 'react'; -import { persistentState } from '../../utils/persistentState.js'; -import { - shouldPromptForTerminalSetup, - terminalSetup, - formatTerminalSetupResultMessage, - TERMINAL_SETUP_CONSENT_MESSAGE, -} from '../utils/terminalSetup.js'; -import { requestConsentInteractive } from '../../config/extensions/consent.js'; -import type { ConfirmationRequest } from '../types.js'; -import type { UseHistoryManagerReturn } from './useHistoryManager.js'; - -interface UseTerminalSetupPromptParams { - /** Function to add a confirmation request to the UI */ - addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void; - /** History manager to display result messages */ - historyManager: UseHistoryManagerReturn; -} - -/** - * Hook that shows a one-time prompt to run /terminal-setup when it would help. - * - * The prompt is shown only once per user (tracked via persistentState) and only - * when shouldPromptForTerminalSetup() returns true (i.e., in a supported terminal - * without the necessary keybindings configured). - */ -export function useTerminalSetupPrompt({ - addConfirmUpdateExtensionRequest, - historyManager, -}: UseTerminalSetupPromptParams): void { - useEffect(() => { - const hasBeenPrompted = persistentState.get('terminalSetupPromptShown'); - if (hasBeenPrompted) { - return; - } - - let cancelled = false; - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - (async () => { - const shouldPrompt = await shouldPromptForTerminalSetup(); - if (!shouldPrompt || cancelled) return; - - // Record that we've shown the prompt so it never appears again. - persistentState.set('terminalSetupPromptShown', true); - - const confirmed = await requestConsentInteractive( - TERMINAL_SETUP_CONSENT_MESSAGE, - addConfirmUpdateExtensionRequest, - ); - - if (!confirmed || cancelled) return; - - const result = await terminalSetup(); - if (cancelled) return; - - historyManager.addItem( - { - type: result.success ? 'info' : 'error', - text: formatTerminalSetupResultMessage(result), - }, - Date.now(), - ); - })(); - - return () => { - cancelled = true; - }; - }, [addConfirmUpdateExtensionRequest, historyManager]); -} diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index 4c292dff139..09eff97d3ec 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -32,6 +32,11 @@ import { promisify } from 'node:util'; import { terminalCapabilityManager } from './terminalCapabilityManager.js'; import { debugLogger, homedir } from '@google/gemini-cli-core'; +import { useEffect } from 'react'; +import { persistentState } from '../../utils/persistentState.js'; +import { requestConsentInteractive } from '../../config/extensions/consent.js'; +import type { ConfirmationRequest } from '../types.js'; +import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; export const VSCODE_SHIFT_ENTER_SEQUENCE = '\\\r\n'; @@ -58,29 +63,24 @@ type SupportedTerminal = 'vscode' | 'cursor' | 'windsurf' | 'antigravity'; * Terminal metadata used for configuration. */ interface TerminalData { - /** Display name for the terminal (e.g., "VS Code") */ terminalName: string; - /** Application folder name used in config paths (e.g., "Code") */ appName: string; } +const TERMINAL_DATA: Record = { + vscode: { terminalName: 'VS Code', appName: 'Code' }, + cursor: { terminalName: 'Cursor', appName: 'Cursor' }, + windsurf: { terminalName: 'Windsurf', appName: 'Windsurf' }, + antigravity: { terminalName: 'Antigravity', appName: 'Antigravity' }, +}; /** * Maps a supported terminal ID to its display name and config folder name. * Returns null if the terminal is not supported. */ -function getTerminalData(terminal: SupportedTerminal): TerminalData | null { - switch (terminal) { - case 'vscode': - return { terminalName: 'VS Code', appName: 'Code' }; - case 'cursor': - return { terminalName: 'Cursor', appName: 'Cursor' }; - case 'windsurf': - return { terminalName: 'Windsurf', appName: 'Windsurf' }; - case 'antigravity': - return { terminalName: 'Antigravity', appName: 'Antigravity' }; - default: - return null; - } +function getSupportedTerminalData( + terminal: SupportedTerminal, +): TerminalData | null { + return TERMINAL_DATA[terminal] || null; } /** @@ -358,7 +358,7 @@ export async function shouldPromptForTerminalSetup(): Promise { return false; } - const terminalData = getTerminalData(terminal); + const terminalData = getSupportedTerminalData(terminal); if (!terminalData) { return false; } @@ -430,7 +430,7 @@ export async function terminalSetup(): Promise { }; } - const terminalData = getTerminalData(terminal); + const terminalData = getSupportedTerminalData(terminal); if (!terminalData) { return { success: false, @@ -440,3 +440,67 @@ export async function terminalSetup(): Promise { return configureVSCodeStyle(terminalData.terminalName, terminalData.appName); } + +export const TERMINAL_SETUP_CONSENT_MESSAGE = + 'Gemini CLI works best with Shift+Enter/Ctrl+Enter for multiline input. ' + + 'Would you like to automatically configure your terminal keybindings?'; + +export function formatTerminalSetupResultMessage( + result: TerminalSetupResult, +): string { + let content = result.message; + if (result.requiresRestart) { + content += + '\n\nPlease restart your terminal for the changes to take effect.'; + } + return content; +} + +interface UseTerminalSetupPromptParams { + addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void; + historyManager: UseHistoryManagerReturn; +} + +/** + * Hook that shows a one-time prompt to run /terminal-setup when it would help. + */ +export function useTerminalSetupPrompt({ + addConfirmUpdateExtensionRequest, + historyManager, +}: UseTerminalSetupPromptParams): void { + useEffect(() => { + const hasBeenPrompted = persistentState.get('terminalSetupPromptShown'); + if (hasBeenPrompted) { + return; + } + + let cancelled = false; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + (async () => { + const shouldPrompt = await shouldPromptForTerminalSetup(); + if (!shouldPrompt || cancelled) return; + persistentState.set('terminalSetupPromptShown', true); + + const confirmed = await requestConsentInteractive( + TERMINAL_SETUP_CONSENT_MESSAGE, + addConfirmUpdateExtensionRequest, + ); + if (!confirmed || cancelled) return; + + const result = await terminalSetup(); + if (cancelled) return; + historyManager.addItem( + { + type: result.success ? 'info' : 'error', + text: formatTerminalSetupResultMessage(result), + }, + Date.now(), + ); + })(); + + return () => { + cancelled = true; + }; + }, [addConfirmUpdateExtensionRequest, historyManager]); +} From c91080be41cbd4c58966deadabe13f4a3e8a33c7 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 16 Jan 2026 20:43:13 +0530 Subject: [PATCH 5/8] remove comments --- packages/cli/src/ui/utils/terminalSetup.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index 09eff97d3ec..a517c07a1cb 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -75,7 +75,6 @@ const TERMINAL_DATA: Record = { /** * Maps a supported terminal ID to its display name and config folder name. - * Returns null if the terminal is not supported. */ function getSupportedTerminalData( terminal: SupportedTerminal, @@ -83,9 +82,6 @@ function getSupportedTerminalData( return TERMINAL_DATA[terminal] || null; } -/** - * Type for a keybinding entry in keybindings.json - */ type Keybinding = { key?: string; command?: string; @@ -94,10 +90,6 @@ type Keybinding = { /** * Checks if a keybindings array contains our specific binding for a given key. - * Our bindings are identified by: - * - The key ('shift+enter' or 'ctrl+enter') - * - The command 'workbench.action.terminal.sendSequence' - * - The args.text value matching VSCODE_SHIFT_ENTER_SEQUENCE */ function hasOurBinding( keybindings: unknown[], From c72825bba5371b1f3e5916741819d0cd4c761a94 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Wed, 21 Jan 2026 11:14:41 +0530 Subject: [PATCH 6/8] fix bug --- packages/cli/src/ui/utils/terminalSetup.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index a517c07a1cb..a31687ab198 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -472,12 +472,15 @@ export function useTerminalSetupPrompt({ (async () => { const shouldPrompt = await shouldPromptForTerminalSetup(); if (!shouldPrompt || cancelled) return; + + // Set this BEFORE showing the prompt to prevent re-prompting on re-render persistentState.set('terminalSetupPromptShown', true); const confirmed = await requestConsentInteractive( TERMINAL_SETUP_CONSENT_MESSAGE, addConfirmUpdateExtensionRequest, ); + if (!confirmed || cancelled) return; const result = await terminalSetup(); From 8ea2030f959d90c00f9b810feef89002c8e28a32 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Wed, 21 Jan 2026 23:35:06 +0530 Subject: [PATCH 7/8] Remove comment in terminalSetup.ts Remove comment about setting terminal setup prompt state. --- packages/cli/src/ui/utils/terminalSetup.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index a31687ab198..9831937d4e1 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -473,7 +473,6 @@ export function useTerminalSetupPrompt({ const shouldPrompt = await shouldPromptForTerminalSetup(); if (!shouldPrompt || cancelled) return; - // Set this BEFORE showing the prompt to prevent re-prompting on re-render persistentState.set('terminalSetupPromptShown', true); const confirmed = await requestConsentInteractive( From 639a213519b408cf5485603beaa5629163873ea7 Mon Sep 17 00:00:00 2001 From: ved015 Date: Mon, 23 Feb 2026 20:00:45 +0530 Subject: [PATCH 8/8] fix lint errors --- packages/cli/src/ui/utils/terminalSetup.ts | 32 ++++++++++------------ 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index c511872f329..3138d73d058 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -88,6 +88,10 @@ type Keybinding = { args?: { text?: string }; }; +function isKeybinding(kb: unknown): kb is Keybinding { + return typeof kb === 'object' && kb !== null; +} + /** * Checks if a keybindings array contains our specific binding for a given key. */ @@ -96,11 +100,11 @@ function hasOurBinding( key: 'shift+enter' | 'ctrl+enter', ): boolean { return keybindings.some((kb) => { - const binding = kb as Keybinding; + if (!isKeybinding(kb)) return false; return ( - binding.key === key && - binding.command === 'workbench.action.terminal.sendSequence' && - binding.args?.text === VSCODE_SHIFT_ENTER_SEQUENCE + kb.key === key && + kb.command === 'workbench.action.terminal.sendSequence' && + kb.args?.text === VSCODE_SHIFT_ENTER_SEQUENCE ); }); } @@ -254,7 +258,7 @@ async function configureVSCodeStyle( } catch { // File doesn't exist, will create new one } - + const targetBindings = [ { key: 'shift+enter', @@ -296,23 +300,17 @@ async function configureVSCodeStyle( const results = targetBindings.map((target) => { const hasOurBinding = keybindings.some((kb) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const binding = kb as { - command?: string; - args?: { text?: string }; - key?: string; - }; + if (!isKeybinding(kb)) return false; return ( - binding.key === target.key && - binding.command === target.command && - binding.args?.text === target.args.text + kb.key === target.key && + kb.command === target.command && + kb.args?.text === target.args.text ); }); const existingBinding = keybindings.find((kb) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const binding = kb as { key?: string }; - return binding.key === target.key; + if (!isKeybinding(kb)) return false; + return kb.key === target.key; }); return {