@@ -11,6 +11,7 @@ import {
1111 getOverrideConfigAtom ,
1212 getSettingsKeyAtom ,
1313 globalStore ,
14+ isDev ,
1415 openLink ,
1516 setTabIndicator ,
1617 WOS ,
@@ -43,6 +44,7 @@ const TermCacheFileName = "cache:term:full";
4344const MinDataProcessedForCache = 100 * 1024 ;
4445export const SupportsImageInput = true ;
4546const IMEDedupWindowMs = 20 ;
47+ const MaxRepaintTransactionMs = 2000 ;
4648
4749// detect webgl support
4850function detectWebGLSupport ( ) : boolean {
@@ -104,9 +106,23 @@ export class TermWrap {
104106 // xterm.js paste() method triggers onData event, which can cause duplicate sends
105107 lastPasteData : string = "" ;
106108 lastPasteTime : number = 0 ;
109+
110+ // for scrollToBottom support during a resize
107111 lastAtBottomTime : number = Date . now ( ) ;
108112 lastScrollAtBottom : boolean = true ;
109113 cachedAtBottomForResize : boolean | null = null ;
114+ viewportScrollTop : number = 0 ;
115+
116+ // dev only (for debugging)
117+ recentWrites : { idx : number ; data : string ; ts : number } [ ] = [ ] ;
118+ recentWritesCounter : number = 0 ;
119+
120+ // for repaint transaction scrolling behavior
121+ lastClearScrollbackTs : number = 0 ;
122+ lastMode2026SetTs : number = 0 ;
123+ lastMode2026ResetTs : number = 0 ;
124+ inSyncTransaction : boolean = false ;
125+ inRepaintTransaction : boolean = false ;
110126
111127 constructor (
112128 tabId : string ,
@@ -187,6 +203,44 @@ export class TermWrap {
187203 this . terminal . parser . registerOscHandler ( 16162 , ( data : string ) => {
188204 return handleOsc16162Command ( data , this . blockId , this . loaded , this ) ;
189205 } ) ;
206+ this . toDispose . push (
207+ this . terminal . parser . registerCsiHandler ( { final : "J" } , ( params ) => {
208+ if ( params [ 0 ] === 3 ) {
209+ this . lastClearScrollbackTs = Date . now ( ) ;
210+ if ( this . inSyncTransaction ) {
211+ console . log ( "[termwrap] repaint transaction starting" ) ;
212+ this . inRepaintTransaction = true ;
213+ }
214+ }
215+ return false ;
216+ } )
217+ ) ;
218+ this . toDispose . push (
219+ this . terminal . parser . registerCsiHandler ( { prefix : "?" , final : "h" } , ( params ) => {
220+ if ( params [ 0 ] === 2026 ) {
221+ this . lastMode2026SetTs = Date . now ( ) ;
222+ this . inSyncTransaction = true ;
223+ }
224+ return false ;
225+ } )
226+ ) ;
227+ this . toDispose . push (
228+ this . terminal . parser . registerCsiHandler ( { prefix : "?" , final : "l" } , ( params ) => {
229+ if ( params [ 0 ] === 2026 ) {
230+ this . lastMode2026ResetTs = Date . now ( ) ;
231+ this . inSyncTransaction = false ;
232+ const wasRepaint = this . inRepaintTransaction ;
233+ this . inRepaintTransaction = false ;
234+ if ( wasRepaint && Date . now ( ) - this . lastClearScrollbackTs <= MaxRepaintTransactionMs ) {
235+ setTimeout ( ( ) => {
236+ console . log ( "[termwrap] repaint transaction complete, scrolling to bottom" ) ;
237+ this . terminal . scrollToBottom ( ) ;
238+ } , 20 ) ;
239+ }
240+ }
241+ return false ;
242+ } )
243+ ) ;
190244 this . toDispose . push (
191245 this . terminal . onBell ( ( ) => {
192246 if ( ! this . loaded ) {
@@ -231,9 +285,8 @@ export class TermWrap {
231285 } ) ;
232286 const viewportElem = this . connectElem . querySelector ( ".xterm-viewport" ) as HTMLElement ;
233287 if ( viewportElem ) {
234- const scrollHandler = ( ) => {
235- const atBottom = viewportElem . scrollTop + viewportElem . clientHeight >= viewportElem . scrollHeight - 20 ;
236- this . setAtBottom ( atBottom ) ;
288+ const scrollHandler = ( e : any ) => {
289+ this . handleViewportScroll ( viewportElem ) ;
237290 } ;
238291 viewportElem . addEventListener ( "scroll" , scrollHandler ) ;
239292 this . toDispose . push ( {
@@ -416,6 +469,13 @@ export class TermWrap {
416469 }
417470
418471 doTerminalWrite ( data : string | Uint8Array , setPtyOffset ?: number ) : Promise < void > {
472+ if ( isDev ( ) && this . loaded ) {
473+ const dataStr = data instanceof Uint8Array ? new TextDecoder ( ) . decode ( data ) : data ;
474+ this . recentWrites . push ( { idx : this . recentWritesCounter ++ , ts : Date . now ( ) , data : dataStr } ) ;
475+ if ( this . recentWrites . length > 50 ) {
476+ this . recentWrites . shift ( ) ;
477+ }
478+ }
419479 let resolve : ( ) => void = null ;
420480 let prtn = new Promise < void > ( ( presolve , _ ) => {
421481 resolve = presolve ;
@@ -498,6 +558,19 @@ export class TermWrap {
498558 return Date . now ( ) - this . lastAtBottomTime <= 1000 ;
499559 }
500560
561+ handleViewportScroll ( viewportElem : HTMLElement ) {
562+ const { scrollTop, scrollHeight, clientHeight } = viewportElem ;
563+ const atBottom = scrollTop + clientHeight >= scrollHeight - clientHeight * 0.5 ;
564+ this . setAtBottom ( atBottom ) ;
565+ const delta = this . viewportScrollTop - scrollTop ;
566+ if ( isDev ( ) && delta >= 500 ) {
567+ console . log (
568+ `[termwrap] large-scroll blockId=${ this . blockId } delta=${ Math . round ( delta ) } px scrollTop=${ scrollTop } wasNearBottom=${ atBottom } `
569+ ) ;
570+ }
571+ this . viewportScrollTop = scrollTop ;
572+ }
573+
501574 handleResize ( ) {
502575 const oldRows = this . terminal . rows ;
503576 const oldCols = this . terminal . cols ;
@@ -508,6 +581,14 @@ export class TermWrap {
508581 this . fitAddon . fit ( ) ;
509582 if ( oldRows !== this . terminal . rows || oldCols !== this . terminal . cols ) {
510583 const termSize : TermSize = { rows : this . terminal . rows , cols : this . terminal . cols } ;
584+ console . log (
585+ "[termwrap] resize" ,
586+ `${ oldRows } x${ oldCols } ` ,
587+ "->" ,
588+ `${ this . terminal . rows } x${ this . terminal . cols } ` ,
589+ "atBottom:" ,
590+ atBottom
591+ ) ;
511592 RpcApi . ControllerInputCommand ( TabRpcClient , { blockid : this . blockId , termsize : termSize } ) ;
512593 }
513594 dlog ( "resize" , `${ this . terminal . rows } x${ this . terminal . cols } ` , `${ oldRows } x${ oldCols } ` , this . hasResized ) ;
@@ -517,6 +598,7 @@ export class TermWrap {
517598 }
518599 if ( atBottom ) {
519600 setTimeout ( ( ) => {
601+ console . log ( "[termwrap] resize scroll-to-bottom" ) ;
520602 this . cachedAtBottomForResize = null ;
521603 this . terminal . scrollToBottom ( ) ;
522604 this . setAtBottom ( true ) ;
0 commit comments