@@ -665,6 +665,12 @@ 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
668674 /** Images attached with Ctrl+P, sent with the next prompt submit. */
669675 pendingAttachments : PendingImageAttachment [ ]
670676 /** Up/Down recall of messages already sent in this session. */
@@ -695,6 +701,23 @@ export type PrimaryOverlayKind =
695701 | "mcp"
696702 | "plugin_credentials"
697703
704+ // Human keystrokes land tens of milliseconds apart at the fastest; a paste
705+ // replayed onto stdin without bracketed-paste framing lands effectively all
706+ // at once. Anything under this gap between keypresses is paste, not typing.
707+ const PASTE_BURST_MS = 15
708+
709+ /** A single unmodified character, as opposed to a control chord or named key. */
710+ function isPrintableInsertKey ( key : KeyEvent ) : boolean {
711+ return (
712+ ! key . ctrl &&
713+ ! key . meta &&
714+ ! key . option &&
715+ typeof key . sequence === "string" &&
716+ key . sequence . length === 1 &&
717+ key . sequence >= " "
718+ )
719+ }
720+
698721const DEFAULT_TITLE = "corbits"
699722const DEFAULT_OVERLAY_ITEMS = [
700723 "Allow bash: ls" ,
@@ -4666,6 +4689,46 @@ export function createAppShell(
46664689 // kill ring — Ctrl+K/U/W and Alt+D delete natively but discard the text;
46674690 // Ctrl+Y/Alt+Y need somewhere to yank it back from.
46684691 const keyName = typeof key . name === "string" ? key . name . toLowerCase ( ) : ""
4692+
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+
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
4730+ }
4731+
46694732 const isCtrlKillYank =
46704733 key . ctrl &&
46714734 ! key . meta &&
@@ -4984,6 +5047,9 @@ export function createAppShell(
49845047 parentStreamLog : null ,
49855048 parentStreamLogBase : null ,
49865049 promptKillRing : emptyKillRing ,
5050+ lastKeyAt : 0 ,
5051+ lastKeyWasPrintable : false ,
5052+ suppressNextLinefeed : false ,
49875053 pendingAttachments : [ ] ,
49885054 sentHistory : createSentHistoryBrowse ( [ ] ) ,
49895055 disposed : false ,
0 commit comments