@@ -665,12 +665,6 @@ export type AppShell = {
665665 * ./prompt-kill-ring.js).
666666 */
667667 promptKillRing : KillRing
668- /** `Date.now()` of the last keypress; detects an un-bracketed paste burst (see `PASTE_BURST_MS`). */
669- lastKeyAt : number
670- /** Whether that last keypress inserted a plain character (see `isPrintableInsertKey`). */
671- lastKeyWasPrintable : boolean
672- /** A converted CR is about to be followed by its CRLF partner LF; swallow that LF. */
673- suppressNextLinefeed : boolean
674668 /** Images attached with Ctrl+P, sent with the next prompt submit. */
675669 pendingAttachments : PendingImageAttachment [ ]
676670 /** Up/Down recall of messages already sent in this session. */
@@ -4542,6 +4536,21 @@ export function createAppShell(
45424536 session = enqueue ( session , `seed-${ i + 1 } ` )
45434537 }
45444538
4539+ // A real bracketed-paste event proves this terminal negotiates DEC 2004:
4540+ // every paste from here on arrives as one `paste` event, never as raw
4541+ // keystrokes, so the CRLF-submit fallback below has nothing left to guard
4542+ // against and turns itself off for the rest of the session. Terminals that
4543+ // never send one keep the guard, since they've never shown they can do
4544+ // better. Un-bracketed-paste bookkeeping only this key handler reads, so it
4545+ // lives in this closure rather than on the shared AppShell.
4546+ let sawBracketedPaste = false
4547+ let lastKeyAt = 0
4548+ let lastKeyWasPrintable = false
4549+ let suppressNextLinefeed = false
4550+ const onPaste = ( ) : void => {
4551+ sawBracketedPaste = true
4552+ }
4553+
45454554 const onKey = ( key : KeyEvent ) : void => {
45464555 if ( disposed ) return
45474556
@@ -4690,43 +4699,45 @@ export function createAppShell(
46904699 // Ctrl+Y/Alt+Y need somewhere to yank it back from.
46914700 const keyName = typeof key . name === "string" ? key . name . toLowerCase ( ) : ""
46924701
4693- // The LF half of a CRLF pair the block below just turned into a newline:
4694- // without this, "line one\r\nline two" would insert two newlines, one for
4695- // the converted CR and one for the LF arriving right behind it.
4696- const suppressLinefeed = shell . suppressNextLinefeed
4697- shell . suppressNextLinefeed = false
4698- if ( suppressLinefeed && keyName === "linefeed" && ! key . ctrl && ! key . meta && ! key . option ) {
4699- key . preventDefault ( )
4700- return
4701- }
4702+ // Everything below this line is the un-bracketed-paste fallback, and a
4703+ // terminal that has ever fired a real `paste` event has proven it never
4704+ // needs it: every future paste arrives as one `paste` event, not raw
4705+ // keystrokes, so re-running these checks on it would only risk a false
4706+ // positive for no benefit.
4707+ if ( ! sawBracketedPaste ) {
4708+ // The LF half of a CRLF pair the block below just turned into a
4709+ // newline: without this, "line one\r\nline two" would insert two
4710+ // newlines, one for the converted CR and one for the LF right behind it.
4711+ const suppressLinefeed = suppressNextLinefeed
4712+ suppressNextLinefeed = false
4713+ if ( suppressLinefeed && keyName === "linefeed" && ! key . ctrl && ! key . meta && ! key . option ) {
4714+ key . preventDefault ( )
4715+ return
4716+ }
47024717
4703- // A terminal that never negotiated bracketed paste (DEC 2004) hands a
4704- // multi-line paste to us as ordinary keystrokes, CR and all -- and a bare
4705- // CR is the same "return" that submits. Left alone, pasting three lines
4706- // sends three separate messages instead of composing one. Bracketed paste
4707- // delivers the whole blob as one `paste` event and never reaches here, so
4708- // this only fires on the raw-keystroke fallback.
4709- //
4710- // Detecting it needs two signals, not one: a lone fast Enter can happen
4711- // (key rollover, a scripted "send keys"), and a lone printable character
4712- // right before Enter is just typing. What never happens from a human is a
4713- // printable character landing, then Enter, both inside a keystroke burst
4714- // — that shape is unique to a paste being replayed byte-for-byte. Gating
4715- // on both keeps a deliberate Ctrl+J-then-Enter (newline, then send) safe,
4716- // since Ctrl+J is not "a printable character," while still catching
4717- // "...end of line one<CR><LF>line two..." arriving as raw keystrokes.
4718- const now = Date . now ( )
4719- const sincePreviousKey = now - shell . lastKeyAt
4720- const previousKeyWasPrintable = shell . lastKeyWasPrintable
4721- shell . lastKeyAt = now
4722- shell . lastKeyWasPrintable = isPrintableInsertKey ( key )
4723- const isBareReturn =
4724- ! key . ctrl && ! key . meta && ! key . option && ( keyName === "return" || keyName === "kpenter" )
4725- if ( isBareReturn && previousKeyWasPrintable && sincePreviousKey < PASTE_BURST_MS ) {
4726- key . preventDefault ( )
4727- shell . prompt . insertText ( "\n" )
4728- shell . suppressNextLinefeed = true
4729- return
4718+ // A bare CR is the same "return" that submits. Left alone, pasting
4719+ // three lines here sends three separate messages instead of composing
4720+ // one. Detecting it needs two signals, not one: a lone fast Enter can
4721+ // happen (key rollover, a scripted "send keys"), and a lone printable
4722+ // character right before Enter is just typing. What never happens from
4723+ // a human is a printable character landing, then Enter, both inside a
4724+ // keystroke burst -- that shape is unique to a paste being replayed
4725+ // byte-for-byte. Gating on both keeps a deliberate Ctrl+J-then-Enter
4726+ // (newline, then send) safe, since Ctrl+J is not "a printable
4727+ // character," while still catching "...line one<CR><LF>line two...".
4728+ const now = Date . now ( )
4729+ const sincePreviousKey = now - lastKeyAt
4730+ const previousKeyWasPrintable = lastKeyWasPrintable
4731+ lastKeyAt = now
4732+ lastKeyWasPrintable = isPrintableInsertKey ( key )
4733+ const isBareReturn =
4734+ ! key . ctrl && ! key . meta && ! key . option && ( keyName === "return" || keyName === "kpenter" )
4735+ if ( isBareReturn && previousKeyWasPrintable && sincePreviousKey < PASTE_BURST_MS ) {
4736+ key . preventDefault ( )
4737+ shell . prompt . insertText ( "\n" )
4738+ suppressNextLinefeed = true
4739+ return
4740+ }
47304741 }
47314742
47324743 const isCtrlKillYank =
@@ -4989,6 +5000,7 @@ export function createAppShell(
49895000
49905001 if ( wireKeys ) {
49915002 renderer . keyInput . on ( "keypress" , onKey )
5003+ renderer . keyInput . on ( "paste" , onPaste )
49925004 prompt . onSubmit = onEnter
49935005 }
49945006 renderer . on ( CliRenderEvents . FRAME , onFrame )
@@ -5047,9 +5059,6 @@ export function createAppShell(
50475059 parentStreamLog : null ,
50485060 parentStreamLogBase : null ,
50495061 promptKillRing : emptyKillRing ,
5050- lastKeyAt : 0 ,
5051- lastKeyWasPrintable : false ,
5052- suppressNextLinefeed : false ,
50535062 pendingAttachments : [ ] ,
50545063 sentHistory : createSentHistoryBrowse ( [ ] ) ,
50555064 disposed : false ,
@@ -5059,6 +5068,7 @@ export function createAppShell(
50595068 shell . disposed = true
50605069 if ( wireKeys ) {
50615070 renderer . keyInput . off ( "keypress" , onKey )
5071+ renderer . keyInput . off ( "paste" , onPaste )
50625072 prompt . onSubmit = undefined
50635073 }
50645074 renderer . off ( CliRenderEvents . FRAME , onFrame )
0 commit comments