@@ -42,6 +42,7 @@ const TermFileName = "term";
4242const TermCacheFileName = "cache:term:full" ;
4343const MinDataProcessedForCache = 100 * 1024 ;
4444export const SupportsImageInput = true ;
45+ const IMEDedupWindowMs = 20 ;
4546
4647// detect webgl support
4748function detectWebGLSupport ( ) : boolean {
@@ -93,8 +94,6 @@ export class TermWrap {
9394 onLinkHover ?: ( uri : string | null , mouseX : number , mouseY : number ) => void ;
9495
9596 // IME composition state tracking
96- // Prevents duplicate input when switching input methods during composition (e.g., using Capslock)
97- // xterm.js sends data during compositionupdate AND after compositionend, causing duplicates
9897 isComposing : boolean = false ;
9998 composingData : string = "" ;
10099 lastCompositionEnd : number = 0 ;
@@ -205,7 +204,15 @@ export class TermWrap {
205204 return true ;
206205 } )
207206 ) ;
208- this . terminal . attachCustomKeyEventHandler ( waveOptions . keydownHandler ) ;
207+ this . terminal . attachCustomKeyEventHandler ( ( e : KeyboardEvent ) => {
208+ if ( e . isComposing && ! e . ctrlKey && ! e . altKey && ! e . metaKey ) {
209+ return true ;
210+ }
211+ if ( ! waveOptions . keydownHandler ) {
212+ return true ;
213+ }
214+ return waveOptions . keydownHandler ( e ) ;
215+ } ) ;
209216 this . connectElem = connectElem ;
210217 this . mainFileSubject = null ;
211218 this . heldData = [ ] ;
@@ -236,6 +243,9 @@ export class TermWrap {
236243 resetCompositionState ( ) {
237244 this . isComposing = false ;
238245 this . composingData = "" ;
246+ this . lastComposedText = "" ;
247+ this . lastCompositionEnd = 0 ;
248+ this . firstDataAfterCompositionSent = false ;
239249 }
240250
241251 private handleCompositionStart = ( e : CompositionEvent ) => {
@@ -353,30 +363,13 @@ export class TermWrap {
353363 return ;
354364 }
355365
356- // IME Composition Handling
357- // Block all data during composition - only send the final text after compositionend
358- // This prevents xterm.js from sending intermediate composition data (e.g., during compositionupdate)
366+ // IME fix: suppress isComposing=true events unless they immediately follow
367+ // a compositionend (within 20ms). This handles CapsLock input method switching
368+ // where the composition buffer gets flushed as a spurious isComposing=true event
359369 if ( this . isComposing ) {
360- dlog ( "Blocked data during composition:" , data ) ;
361- return ;
362- }
363-
364- // IME Deduplication (for Capslock input method switching)
365- // When switching input methods with Capslock during composition, some systems send the
366- // composed text twice. We allow the first send and block subsequent duplicates.
367- const IMEDedupWindowMs = 50 ;
368- const now = Date . now ( ) ;
369- const timeSinceCompositionEnd = now - this . lastCompositionEnd ;
370- if ( timeSinceCompositionEnd < IMEDedupWindowMs && data === this . lastComposedText && this . lastComposedText ) {
371- if ( ! this . firstDataAfterCompositionSent ) {
372- // First send after composition - allow it but mark as sent
373- this . firstDataAfterCompositionSent = true ;
374- dlog ( "First data after composition, allowing:" , data ) ;
375- } else {
376- // Second send of the same data - this is a duplicate from Capslock switching, block it
377- dlog ( "Blocked duplicate IME data:" , data ) ;
378- this . lastComposedText = "" ; // Clear to allow same text to be typed again later
379- this . firstDataAfterCompositionSent = false ;
370+ const timeSinceCompositionEnd = Date . now ( ) - this . lastCompositionEnd ;
371+ if ( timeSinceCompositionEnd > IMEDedupWindowMs ) {
372+ dlog ( "Suppressed IME data (composing, not near compositionend):" , data ) ;
380373 return ;
381374 }
382375 }
0 commit comments