@@ -652,6 +652,12 @@ export type AppShell = {
652652 * ./prompt-kill-ring.js).
653653 */
654654 promptKillRing : KillRing
655+ /** `Date.now()` of the last keypress; detects an un-bracketed paste burst (see `PASTE_BURST_MS`). */
656+ lastKeyAt : number
657+ /** Whether that last keypress inserted a plain character (see `isPrintableInsertKey`). */
658+ lastKeyWasPrintable : boolean
659+ /** A converted CR is about to be followed by its CRLF partner LF; swallow that LF. */
660+ suppressNextLinefeed : boolean
655661 /** Images attached with Ctrl+P, sent with the next prompt submit. */
656662 pendingAttachments : PendingImageAttachment [ ]
657663 /** Up/Down recall of messages already sent in this session. */
@@ -682,6 +688,23 @@ export type PrimaryOverlayKind =
682688 | "mcp"
683689 | "plugin_credentials"
684690
691+ // Human keystrokes land tens of milliseconds apart at the fastest; a paste
692+ // replayed onto stdin without bracketed-paste framing lands effectively all
693+ // at once. Anything under this gap between keypresses is paste, not typing.
694+ const PASTE_BURST_MS = 15
695+
696+ /** A single unmodified character, as opposed to a control chord or named key. */
697+ function isPrintableInsertKey ( key : KeyEvent ) : boolean {
698+ return (
699+ ! key . ctrl &&
700+ ! key . meta &&
701+ ! key . option &&
702+ typeof key . sequence === "string" &&
703+ key . sequence . length === 1 &&
704+ key . sequence >= " "
705+ )
706+ }
707+
685708const DEFAULT_TITLE = "corbits"
686709const DEFAULT_OVERLAY_ITEMS = [
687710 "Allow bash: ls" ,
@@ -4586,6 +4609,46 @@ export function createAppShell(
45864609 // kill ring — Ctrl+K/U/W and Alt+D delete natively but discard the text;
45874610 // Ctrl+Y/Alt+Y need somewhere to yank it back from.
45884611 const keyName = typeof key . name === "string" ? key . name . toLowerCase ( ) : ""
4612+
4613+ // The LF half of a CRLF pair the block below just turned into a newline:
4614+ // without this, "line one\r\nline two" would insert two newlines, one for
4615+ // the converted CR and one for the LF arriving right behind it.
4616+ const suppressLinefeed = shell . suppressNextLinefeed
4617+ shell . suppressNextLinefeed = false
4618+ if ( suppressLinefeed && keyName === "linefeed" && ! key . ctrl && ! key . meta && ! key . option ) {
4619+ key . preventDefault ( )
4620+ return
4621+ }
4622+
4623+ // A terminal that never negotiated bracketed paste (DEC 2004) hands a
4624+ // multi-line paste to us as ordinary keystrokes, CR and all -- and a bare
4625+ // CR is the same "return" that submits. Left alone, pasting three lines
4626+ // sends three separate messages instead of composing one. Bracketed paste
4627+ // delivers the whole blob as one `paste` event and never reaches here, so
4628+ // this only fires on the raw-keystroke fallback.
4629+ //
4630+ // Detecting it needs two signals, not one: a lone fast Enter can happen
4631+ // (key rollover, a scripted "send keys"), and a lone printable character
4632+ // right before Enter is just typing. What never happens from a human is a
4633+ // printable character landing, then Enter, both inside a keystroke burst
4634+ // — that shape is unique to a paste being replayed byte-for-byte. Gating
4635+ // on both keeps a deliberate Ctrl+J-then-Enter (newline, then send) safe,
4636+ // since Ctrl+J is not "a printable character," while still catching
4637+ // "...end of line one<CR><LF>line two..." arriving as raw keystrokes.
4638+ const now = Date . now ( )
4639+ const sincePreviousKey = now - shell . lastKeyAt
4640+ const previousKeyWasPrintable = shell . lastKeyWasPrintable
4641+ shell . lastKeyAt = now
4642+ shell . lastKeyWasPrintable = isPrintableInsertKey ( key )
4643+ const isBareReturn =
4644+ ! key . ctrl && ! key . meta && ! key . option && ( keyName === "return" || keyName === "kpenter" )
4645+ if ( isBareReturn && previousKeyWasPrintable && sincePreviousKey < PASTE_BURST_MS ) {
4646+ key . preventDefault ( )
4647+ shell . prompt . insertText ( "\n" )
4648+ shell . suppressNextLinefeed = true
4649+ return
4650+ }
4651+
45894652 const isCtrlKillYank =
45904653 key . ctrl &&
45914654 ! key . meta &&
@@ -4902,6 +4965,9 @@ export function createAppShell(
49024965 observe : null ,
49034966 parentStreamLog : null ,
49044967 promptKillRing : emptyKillRing ,
4968+ lastKeyAt : 0 ,
4969+ lastKeyWasPrintable : false ,
4970+ suppressNextLinefeed : false ,
49054971 pendingAttachments : [ ] ,
49064972 sentHistory : createSentHistoryBrowse ( [ ] ) ,
49074973 disposed : false ,
0 commit comments