diff --git a/src/renderer/src/settings/SettingsTabs.tsx b/src/renderer/src/settings/SettingsTabs.tsx index 10d9a22..5753e42 100644 --- a/src/renderer/src/settings/SettingsTabs.tsx +++ b/src/renderer/src/settings/SettingsTabs.tsx @@ -270,7 +270,7 @@ export function RenderSettingsTab(): React.JSX.Element { /> 100 +/** + * Paste-risk heuristic, split by shape rather than by raw length: + * - two or more lines always confirm — a stray newline executes a command + * the user never reviewed; + * - a single line pastes straight through, unless it is unusually long + * (those are pasted scripts rather than something typed by hand). + */ +const SINGLE_LINE_CONFIRM_LENGTH = 1000 +function needsPasteConfirm(text: string): boolean { + const normalized = text.replace(/\r\n?/g, '\n').replace(/\n$/, '') + if (normalized.includes('\n')) return true + return normalized.length > SINGLE_LINE_CONFIRM_LENGTH } /** @@ -511,19 +519,17 @@ export const TerminalView: ForwardRefExoticComponent { - if ( - !deadRef.current && - settings.terminal.pasteRiskConfirm && - !pasteConfirmedForSession && - isRiskyPaste(text) - ) { + // Read the setting at call time: this runs from a long-lived keydown + // listener, so a captured value would go stale after a settings change. + const confirmOn = useSettingsStore.getState().settings.terminal.pasteRiskConfirm + if (!deadRef.current && confirmOn && !pasteConfirmedForSession && needsPasteConfirm(text)) { nativePasteRef.current = text setPaste({ noPrompt: false, disableDetection: false }) return } confirmPaste(text) }, - [settings.terminal.pasteRiskConfirm, confirmPaste] + [confirmPaste] ) /** Confirm the risky-paste dialog: apply its choices, then paste. */ @@ -873,9 +879,36 @@ export const TerminalView: ForwardRefExoticComponent { + if (e.type !== 'keydown' || e.altKey) return + const mod = navigator.platform.toLowerCase().includes('mac') ? e.metaKey : e.ctrlKey + if (!mod || e.shiftKey || e.key.toLowerCase() !== 'v') return + const target = e.target as HTMLElement | null + const isXtermHelper = + target instanceof HTMLTextAreaElement && target.classList.contains('xterm-helper-textarea') + if ( + target && + !isXtermHelper && + (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) + ) { + return // ordinary inputs keep the browser's native paste + } + e.preventDefault() + e.stopPropagation() + void pasteFromClipboard() + } + host?.addEventListener('keydown', onPasteKey, true) + return () => { if (carryTimer !== undefined) window.clearTimeout(carryTimer) if (ptyTimerRef.current) window.clearTimeout(ptyTimerRef.current) + host?.removeEventListener('keydown', onPasteKey, true) for (const unsubscribe of unsubscribes) unsubscribe() for (const disposable of disposables) disposable.dispose() observer?.disconnect()