@@ -122,21 +122,45 @@ Object.assign(CodemanApp.prototype, {
122122 // On Android Chrome, typing symbols (e.g., "/" from Gboard's symbol keyboard)
123123 // sends keyCode 229 + input event WITHOUT compositionstart/end wrapping.
124124 // The custom key handler above returns false for keyCode 229, telling xterm
125- // to ignore the keydown. This listener catches those orphaned input events.
125+ // to ignore the keydown. xterm.js expects the character to arrive via
126+ // composition events, but since there's no composition, the character is lost.
127+ // This listener catches those orphaned input events and forwards them to onData.
126128 {
127129 const xtermTextarea = container . querySelector ( '.xterm-helper-textarea' ) ;
128130 if ( xtermTextarea && MobileDetection . isTouchDevice ( ) ) {
129131 let composing = false ;
132+ let lastKeydownHandled = 0 ;
130133 xtermTextarea . addEventListener ( 'compositionstart' , ( ) => { composing = true ; } ) ;
131134 xtermTextarea . addEventListener ( 'compositionend' , ( ) => { composing = false ; } ) ;
135+ // Track when xterm handles a keydown normally (non-229 keyCode).
136+ // If xterm processed the keydown, it will emit onData itself --
137+ // the input event handler below must NOT re-send the character.
138+ xtermTextarea . addEventListener ( 'keydown' , ( e ) => {
139+ if ( ! e . isComposing && e . keyCode !== 229 ) {
140+ lastKeydownHandled = Date . now ( ) ;
141+ }
142+ } ) ;
132143 xtermTextarea . addEventListener ( 'input' , ( e ) => {
144+ // Only handle insertText events outside of composition -- these are
145+ // the ones xterm.js misses on Android virtual keyboards.
133146 if ( composing || e . isComposing ) return ;
134147 if ( e . inputType !== 'insertText' || ! e . data ) return ;
148+ // If xterm just handled a keydown (within 50ms), it already sent the
149+ // char via onData. Skip to avoid double-send (e.g., Shift+A => AA).
150+ if ( Date . now ( ) - lastKeydownHandled < 50 ) return ;
151+ // xterm.js may have already processed this via its own input handler.
152+ // Check if the textarea was cleared by xterm (value is empty or just
153+ // whitespace) -- if so, xterm handled it and we should not double-send.
154+ // Use a microtask to check after xterm's own handlers have run.
135155 const data = e . data ;
136156 Promise . resolve ( ) . then ( ( ) => {
157+ // If xterm cleared the textarea, it processed the input -- skip.
137158 const val = xtermTextarea . value ;
138159 if ( ! val || val . trim ( ) === '' ) return ;
160+ // xterm didn't process it -- forward to terminal as if typed.
161+ // Emit via onData path by writing to terminal's input handler.
139162 this . terminal . _core . coreService . triggerDataEvent ( data , true ) ;
163+ // Clear the textarea to prevent xterm from processing it later.
140164 xtermTextarea . value = '' ;
141165 } ) ;
142166 } ) ;
0 commit comments