diff --git a/integration-tests/context-compress-interactive.test.ts b/integration-tests/context-compress-interactive.test.ts index 49f5e2aa7c..c7e04c6c23 100644 --- a/integration-tests/context-compress-interactive.test.ts +++ b/integration-tests/context-compress-interactive.test.ts @@ -32,7 +32,7 @@ describe('Interactive Mode', () => { await run.sendKeys( 'Write a 200 word story about a robot. The story MUST end with the text THE_END followed by a period.', ); - await run.sendKeys('\r'); + await run.type('\r'); // Wait for the specific end marker. await run.expectText('THE_END.', 30000); diff --git a/integration-tests/extensions-reload.test.ts b/integration-tests/extensions-reload.test.ts index 29db8522ad..520076d7c6 100644 --- a/integration-tests/extensions-reload.test.ts +++ b/integration-tests/extensions-reload.test.ts @@ -95,7 +95,10 @@ describe('extension reloading', () => { // Poll for the updated list await rig.pollCommand( - () => run.sendKeys('\u0015/mcp list\r'), + async () => { + await run.sendText('/mcp list'); + await run.type('\r'); + }, () => { const output = stripAnsi(run.output); return ( @@ -110,9 +113,9 @@ describe('extension reloading', () => { // Update the extension, expect the list to update, and mcp servers as well. await run.sendKeys('\u0015/extensions update test-extension'); await run.expectText('/extensions update test-extension'); - await run.sendKeys('\r'); + await run.type('\r'); await new Promise((resolve) => setTimeout(resolve, 500)); - await run.sendKeys('\r'); + await run.type('\r'); await run.expectText( ` * test-server (remote): http://localhost:${portB}/mcp`, ); @@ -123,7 +126,10 @@ describe('extension reloading', () => { // Poll for the updated extension version await rig.pollCommand( - () => run.sendKeys('\u0015/extensions list\r'), + async () => { + await run.sendText('/extensions list'); + await run.type('\r'); + }, () => stripAnsi(run.output).includes( 'test-extension (v0.0.2) - active (updated)', @@ -133,7 +139,10 @@ describe('extension reloading', () => { // Poll for the updated mcp tool await rig.pollCommand( - () => run.sendKeys('\u0015/mcp list\r'), + async () => { + await run.sendText('/mcp list'); + await run.type('\r'); + }, () => { const output = stripAnsi(run.output); return ( @@ -146,7 +155,7 @@ describe('extension reloading', () => { ); await run.sendText('/quit'); - await run.sendKeys('\r'); + await run.type('\r'); // Clean things up. await serverA.stop(); diff --git a/integration-tests/file-system-interactive.test.ts b/integration-tests/file-system-interactive.test.ts index d7ad73fd0d..6f955a1378 100644 --- a/integration-tests/file-system-interactive.test.ts +++ b/integration-tests/file-system-interactive.test.ts @@ -37,7 +37,7 @@ describe('Interactive file system', () => { // Step 1: Read the file const readPrompt = `Read the version from ${fileName}`; await run.type(readPrompt); - await run.sendKeys('\r'); + await run.type('\r'); const readCall = await rig.waitForToolCall('read_file', 30000); expect(readCall, 'Expected to find a read_file tool call').toBe(true); @@ -45,7 +45,7 @@ describe('Interactive file system', () => { // Step 2: Write the file const writePrompt = `now change the version to 1.0.1 in the file`; await run.type(writePrompt); - await run.sendKeys('\r'); + await run.type('\r'); // Check tool calls made with right args await rig.expectToolCallSuccess( diff --git a/integration-tests/hooks-system.test.ts b/integration-tests/hooks-system.test.ts index c5ab2b024d..34827a5f7c 100644 --- a/integration-tests/hooks-system.test.ts +++ b/integration-tests/hooks-system.test.ts @@ -1087,7 +1087,7 @@ console.log(JSON.stringify({ // Send a prompt to establish a session and trigger an API call await run.sendKeys('Hello'); - await run.sendKeys('\r'); + await run.type('\r'); // Wait for response to ensure API call happened await run.expectText('Hello', 15000); @@ -1166,7 +1166,7 @@ console.log(JSON.stringify({ // Send an initial prompt to establish a session await run.sendKeys('Say hello'); - await run.sendKeys('\r'); + await run.type('\r'); // Wait for the response await run.expectText('Hello', 10000); @@ -1176,14 +1176,14 @@ console.log(JSON.stringify({ const numClears = 3; for (let i = 0; i < numClears; i++) { await run.sendKeys('/clear'); - await run.sendKeys('\r'); + await run.type('\r'); // Wait a bit for clear to complete await new Promise((resolve) => setTimeout(resolve, 2000)); // Send a prompt to establish an active session before next clear await run.sendKeys('Say hello'); - await run.sendKeys('\r'); + await run.type('\r'); // Wait for response await run.expectText('Hello', 10000); diff --git a/integration-tests/mcp_server_cyclic_schema.test.ts b/integration-tests/mcp_server_cyclic_schema.test.ts index 742a35fe78..29373dbac4 100644 --- a/integration-tests/mcp_server_cyclic_schema.test.ts +++ b/integration-tests/mcp_server_cyclic_schema.test.ts @@ -200,7 +200,7 @@ describe('mcp server with cyclic tool schema is detected', () => { const run = await rig.runInteractive(); await run.type('/mcp list'); - await run.sendKeys('\r'); + await run.type('\r'); await run.expectText('tool_with_cyclic_schema'); }); diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts index e197c724a5..8fc4208ebf 100644 --- a/integration-tests/test-helper.ts +++ b/integration-tests/test-helper.ts @@ -209,6 +209,12 @@ export class InteractiveRun { async type(text: string) { let typedSoFar = ''; for (const char of text) { + if (char === '\r') { + // wait >30ms before `enter` to avoid fast return conversion + // from bufferFastReturn() in KeypressContent.tsx + await new Promise((resolve) => setTimeout(resolve, 50)); + } + this.ptyProcess.write(char); typedSoFar += char; diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index a7ac42ce66..ce5654b443 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -91,7 +91,6 @@ import { useVim } from './hooks/vim.js'; import { type LoadableSettingScope, SettingScope } from '../config/settings.js'; import { type InitializationResult } from '../core/initializer.js'; import { useFocus } from './hooks/useFocus.js'; -import { useBracketedPaste } from './hooks/useBracketedPaste.js'; import { useKeypress, type Key } from './hooks/useKeypress.js'; import { keyMatchers, Command } from './keyMatchers.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; @@ -123,7 +122,6 @@ import { useAlternateBuffer } from './hooks/useAlternateBuffer.js'; import { useSettings } from './contexts/SettingsContext.js'; import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'; import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; -import { enableBracketedPaste } from './utils/bracketedPaste.js'; import { useBanner } from './hooks/useBanner.js'; const WARNING_PROMPT_DURATION_MS = 1000; @@ -424,8 +422,7 @@ export const AppContainer = (props: AppContainerProps) => { disableLineWrapping(); app.rerender(); } - enableBracketedPaste(); - terminalCapabilityManager.enableKittyProtocol(); + terminalCapabilityManager.enableSupportedModes(); refreshStatic(); }, [refreshStatic, isAlternateBuffer, app, config]); @@ -925,7 +922,6 @@ Logging in with Google... Restarting Gemini CLI to continue. }); const isFocused = useFocus(); - useBracketedPaste(); // Context file names computation const contextFileNames = useMemo(() => { diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 35ac7d4a71..0c273cac86 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -31,6 +31,7 @@ import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion. import clipboardy from 'clipboardy'; import * as clipboardUtils from '../utils/clipboardUtils.js'; import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js'; +import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import stripAnsi from 'strip-ansi'; import chalk from 'chalk'; @@ -121,6 +122,10 @@ describe('InputPrompt', () => { beforeEach(() => { vi.resetAllMocks(); + vi.spyOn( + terminalCapabilityManager, + 'isBracketedPasteEnabled', + ).mockReturnValue(true); mockCommandContext = createMockCommandContext(); diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 66ad349a9a..82750be0ee 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -35,6 +35,7 @@ import { type SettingDefinition, type SettingsSchemaType, } from '../../config/settingsSchema.js'; +import { terminalCapabilityManager } from '../../ui/utils/terminalCapabilityManager.js'; // Mock the VimModeContext const mockToggleVimEnabled = vi.fn(); @@ -253,6 +254,10 @@ const renderDialog = ( describe('SettingsDialog', () => { beforeEach(() => { + vi.spyOn( + terminalCapabilityManager, + 'isBracketedPasteEnabled', + ).mockReturnValue(true); mockToggleVimEnabled.mockResolvedValue(true); }); diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 1c88e128c7..8bfe51694f 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -19,10 +19,12 @@ import { ESC } from '../utils/input.js'; import { parseMouseEvent } from '../utils/mouse.js'; import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js'; import { appEvents, AppEvent } from '../../utils/events.js'; +import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js'; export const BACKSLASH_ENTER_TIMEOUT = 5; export const ESC_TIMEOUT = 50; export const PASTE_TIMEOUT = 30_000; +export const FAST_RETURN_TIMEOUT = 30; // Parse the key itself const KEY_INFO_MAP: Record< @@ -148,7 +150,7 @@ function nonKeyboardEventFilter( */ function bufferBackslashEnter( keypressHandler: KeypressHandler, -): (key: Key | null) => void { +): KeypressHandler { const bufferer = (function* (): Generator { while (true) { const key = yield; @@ -184,7 +186,31 @@ function bufferBackslashEnter( bufferer.next(); // prime the generator so it starts listening. - return (key: Key | null) => bufferer.next(key); + return (key: Key) => bufferer.next(key); +} + +/** + * Converts return keys pressed quickly after other keys into plain + * insertable return characters. + * + * This is to accomodate older terminals that paste text without bracketing. + */ +function bufferFastReturn(keypressHandler: KeypressHandler): KeypressHandler { + let lastKeyTime = 0; + return (key: Key) => { + const now = Date.now(); + if (key.name === 'return' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) { + keypressHandler({ + ...key, + name: '', + sequence: '\r', + insertable: true, + }); + } else { + keypressHandler(key); + } + lastKeyTime = now; + }; } /** @@ -192,9 +218,7 @@ function bufferBackslashEnter( * Will flush the buffer if no data is received for PASTE_TIMEOUT ms or * when a null key is received. */ -function bufferPaste( - keypressHandler: KeypressHandler, -): (key: Key | null) => void { +function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler { const bufferer = (function* (): Generator { while (true) { let key = yield; @@ -238,7 +262,7 @@ function bufferPaste( })(); bufferer.next(); // prime the generator so it starts listening. - return (key: Key | null) => bufferer.next(key); + return (key: Key) => bufferer.next(key); } /** @@ -592,10 +616,13 @@ export function KeypressProvider({ process.stdin.setEncoding('utf8'); // Make data events emit strings - const mouseFilterer = nonKeyboardEventFilter(broadcast); - const backslashBufferer = bufferBackslashEnter(mouseFilterer); - const pasteBufferer = bufferPaste(backslashBufferer); - let dataListener = createDataListener(pasteBufferer); + let processor = nonKeyboardEventFilter(broadcast); + if (!terminalCapabilityManager.isBracketedPasteEnabled()) { + processor = bufferFastReturn(processor); + } + processor = bufferBackslashEnter(processor); + processor = bufferPaste(processor); + let dataListener = createDataListener(processor); if (debugKeystrokeLogging) { const old = dataListener; diff --git a/packages/cli/src/ui/hooks/useBracketedPaste.ts b/packages/cli/src/ui/hooks/useBracketedPaste.ts deleted file mode 100644 index 1e9cbbebcf..0000000000 --- a/packages/cli/src/ui/hooks/useBracketedPaste.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect } from 'react'; -import { - disableBracketedPaste, - enableBracketedPaste, -} from '../utils/bracketedPaste.js'; - -/** - * Enables and disables bracketed paste mode in the terminal. - * - * This hook ensures that bracketed paste mode is enabled when the component - * mounts and disabled when it unmounts or when the process exits. - */ -export const useBracketedPaste = () => { - const cleanup = () => { - disableBracketedPaste(); - }; - - useEffect(() => { - enableBracketedPaste(); - - process.on('exit', cleanup); - process.on('SIGINT', cleanup); - process.on('SIGTERM', cleanup); - - return () => { - cleanup(); - process.removeListener('exit', cleanup); - process.removeListener('SIGINT', cleanup); - process.removeListener('SIGTERM', cleanup); - }; - }, []); -}; diff --git a/packages/cli/src/ui/utils/bracketedPaste.ts b/packages/cli/src/ui/utils/bracketedPaste.ts deleted file mode 100644 index 26bb0e08fa..0000000000 --- a/packages/cli/src/ui/utils/bracketedPaste.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { writeToStdout } from '@google/gemini-cli-core'; - -const ENABLE_BRACKETED_PASTE = '\x1b[?2004h'; -const DISABLE_BRACKETED_PASTE = '\x1b[?2004l'; - -export const enableBracketedPaste = () => { - writeToStdout(ENABLE_BRACKETED_PASTE); -}; - -export const disableBracketedPaste = () => { - writeToStdout(DISABLE_BRACKETED_PASTE); -}; diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts index 42f59f95a1..bd58fc5cab 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts @@ -23,6 +23,8 @@ vi.mock('@google/gemini-cli-core', () => ({ disableKittyKeyboardProtocol: vi.fn(), enableModifyOtherKeys: vi.fn(), disableModifyOtherKeys: vi.fn(), + enableBracketedPasteMode: vi.fn(), + disableBracketedPasteMode: vi.fn(), })); describe('TerminalCapabilityManager', () => { @@ -264,4 +266,46 @@ describe('TerminalCapabilityManager', () => { expect(manager.isModifyOtherKeysEnabled()).toBe(true); }); }); + + describe('bracketed paste detection', () => { + it('should detect bracketed paste support (mode set)', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Simulate bracketed paste response: \x1b[?2004;1$y + stdin.emit('data', Buffer.from('\x1b[?2004;1$y')); + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isBracketedPasteSupported()).toBe(true); + expect(manager.isBracketedPasteEnabled()).toBe(true); + }); + + it('should detect bracketed paste support (mode reset)', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Simulate bracketed paste response: \x1b[?2004;2$y + stdin.emit('data', Buffer.from('\x1b[?2004;2$y')); + // Complete detection with DA1 + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isBracketedPasteSupported()).toBe(true); + expect(manager.isBracketedPasteEnabled()).toBe(true); + }); + + it('should not enable bracketed paste if not supported', async () => { + const manager = TerminalCapabilityManager.getInstance(); + const promise = manager.detectCapabilities(); + + // Complete detection with DA1 only + stdin.emit('data', Buffer.from('\x1b[?62c')); + + await promise; + expect(manager.isBracketedPasteSupported()).toBe(false); + expect(manager.isBracketedPasteEnabled()).toBe(false); + }); + }); }); diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.ts index c893c46043..f6838a79a0 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.ts @@ -11,6 +11,8 @@ import { disableKittyKeyboardProtocol, enableModifyOtherKeys, disableModifyOtherKeys, + enableBracketedPasteMode, + disableBracketedPasteMode, } from '@google/gemini-cli-core'; export type TerminalBackgroundColor = string | undefined; @@ -23,6 +25,7 @@ export class TerminalCapabilityManager { private static readonly TERMINAL_NAME_QUERY = '\x1b[>q'; private static readonly DEVICE_ATTRIBUTES_QUERY = '\x1b[c'; private static readonly MODIFY_OTHER_KEYS_QUERY = '\x1b[>4;?m'; + private static readonly BRACKETED_PASTE_QUERY = '\x1b[?2004$p'; // Kitty keyboard flags: CSI ? flags u // eslint-disable-next-line no-control-regex @@ -40,6 +43,10 @@ export class TerminalCapabilityManager { // modifyOtherKeys response: CSI > 4 ; level m // eslint-disable-next-line no-control-regex private static readonly MODIFY_OTHER_KEYS_REGEX = /\x1b\[>4;(\d+)m/; + // DECRQM response for bracketed paste: CSI ? 2004 ; Ps $ y + // Ps = 1 (set), 2 (reset), 3 (permanently set), 4 (permanently reset) + // eslint-disable-next-line no-control-regex + private static readonly BRACKETED_PASTE_REGEX = /\x1b\[\?2004;([1-4])\$y/; private terminalBackgroundColor: TerminalBackgroundColor; private kittySupported = false; @@ -48,6 +55,8 @@ export class TerminalCapabilityManager { private terminalName: string | undefined; private modifyOtherKeysSupported = false; private modifyOtherKeysEnabled = false; + private bracketedPasteSupported = false; + private bracketedPasteEnabled = false; private constructor() {} @@ -75,6 +84,21 @@ export class TerminalCapabilityManager { return; } + const cleanupOnExit = () => { + if (this.kittySupported) { + this.disableKittyProtocol(); + } + if (this.modifyOtherKeysSupported) { + this.disableModifyOtherKeys(); + } + if (this.bracketedPasteSupported) { + this.disableBracketedPaste(); + } + }; + process.on('exit', () => cleanupOnExit); + process.on('SIGTERM', () => cleanupOnExit); + process.on('SIGINT', cleanupOnExit); + return new Promise((resolve) => { const originalRawMode = process.stdin.isRaw; if (!originalRawMode) { @@ -87,6 +111,7 @@ export class TerminalCapabilityManager { let deviceAttributesReceived = false; let bgReceived = false; let modifyOtherKeysReceived = false; + let bracketedPasteReceived = false; // eslint-disable-next-line prefer-const let timeoutId: NodeJS.Timeout; @@ -100,27 +125,14 @@ export class TerminalCapabilityManager { } this.detectionComplete = true; - // Auto-enable kitty if supported - if (this.kittySupported) { - this.enableKittyProtocol(); - process.on('exit', () => this.disableKittyProtocol()); - process.on('SIGTERM', () => this.disableKittyProtocol()); - } else if (this.modifyOtherKeysSupported) { - this.enableModifyOtherKeys(); - process.on('exit', () => this.disableModifyOtherKeys()); - process.on('SIGTERM', () => this.disableModifyOtherKeys()); - } + this.enableSupportedModes(); resolve(); }; - const onTimeout = () => { - cleanup(); - }; - // A somewhat long timeout is acceptable as all terminals should respond // to the device attributes query used as a sentinel. - timeoutId = setTimeout(onTimeout, 1000); + timeoutId = setTimeout(cleanup, 1000); const onData = (data: Buffer) => { buffer += data.toString(); @@ -149,6 +161,32 @@ export class TerminalCapabilityManager { this.kittySupported = true; } + // check for modifyOtherKeys support + if (!modifyOtherKeysReceived) { + const match = buffer.match( + TerminalCapabilityManager.MODIFY_OTHER_KEYS_REGEX, + ); + if (match) { + modifyOtherKeysReceived = true; + const level = parseInt(match[1], 10); + this.modifyOtherKeysSupported = level >= 2; + debugLogger.log( + `Detected modifyOtherKeys support: ${this.modifyOtherKeysSupported} (level ${level})`, + ); + } + } + + // check for bracketed paste support + if (!bracketedPasteReceived) { + const match = buffer.match( + TerminalCapabilityManager.BRACKETED_PASTE_REGEX, + ); + if (match) { + bracketedPasteReceived = true; + this.bracketedPasteSupported = true; + } + } + // Check for Terminal Name/Version response. if (!terminalNameReceived) { const match = buffer.match( @@ -174,21 +212,6 @@ export class TerminalCapabilityManager { cleanup(); } } - - // check for modifyOtherKeys support - if (!modifyOtherKeysReceived) { - const match = buffer.match( - TerminalCapabilityManager.MODIFY_OTHER_KEYS_REGEX, - ); - if (match) { - modifyOtherKeysReceived = true; - const level = parseInt(match[1], 10); - this.modifyOtherKeysSupported = level >= 2; - debugLogger.log( - `Detected modifyOtherKeys support: ${this.modifyOtherKeysSupported} (level ${level})`, - ); - } - } }; process.stdin.on('data', onData); @@ -200,6 +223,7 @@ export class TerminalCapabilityManager { TerminalCapabilityManager.OSC_11_QUERY + TerminalCapabilityManager.TERMINAL_NAME_QUERY + TerminalCapabilityManager.MODIFY_OTHER_KEYS_QUERY + + TerminalCapabilityManager.BRACKETED_PASTE_QUERY + TerminalCapabilityManager.DEVICE_ATTRIBUTES_QUERY, ); } catch (e) { @@ -209,6 +233,17 @@ export class TerminalCapabilityManager { }); } + enableSupportedModes() { + if (this.kittySupported) { + this.enableKittyProtocol(); + } else if (this.modifyOtherKeysSupported) { + this.enableModifyOtherKeys(); + } + if (this.bracketedPasteSupported) { + this.enableBracketedPaste(); + } + } + getTerminalBackgroundColor(): TerminalBackgroundColor { return this.terminalBackgroundColor; } @@ -221,6 +256,36 @@ export class TerminalCapabilityManager { return this.kittyEnabled; } + isBracketedPasteSupported(): boolean { + return this.bracketedPasteSupported; + } + + isBracketedPasteEnabled(): boolean { + return this.bracketedPasteEnabled; + } + + enableBracketedPaste(): void { + try { + if (this.bracketedPasteSupported) { + enableBracketedPasteMode(); + this.bracketedPasteEnabled = true; + } + } catch (e) { + debugLogger.warn('Failed to enable bracketed paste mode:', e); + } + } + + disableBracketedPaste(): void { + try { + if (this.bracketedPasteEnabled) { + disableBracketedPasteMode(); + this.bracketedPasteEnabled = false; + } + } catch (e) { + debugLogger.warn('Failed to disable bracketed paste mode:', e); + } + } + enableKittyProtocol(): void { try { if (this.kittySupported) { diff --git a/packages/core/src/utils/terminal.ts b/packages/core/src/utils/terminal.ts index f8070a4a37..5e2fdb8bf0 100644 --- a/packages/core/src/utils/terminal.ts +++ b/packages/core/src/utils/terminal.ts @@ -34,6 +34,14 @@ export function disableModifyOtherKeys() { writeToStdout('\x1b[>4;0m'); } +export function enableBracketedPasteMode() { + writeToStdout('\x1b[?2004h'); +} + +export function disableBracketedPasteMode() { + writeToStdout('\x1b[?2004l'); +} + export function enableLineWrapping() { writeToStdout('\x1b[?7h'); }