@@ -104,10 +104,6 @@ define(function (require, exports, module) {
104104 this . searchAddon = null ;
105105 this . $container = null ;
106106 this . _resizeTimeout = null ;
107- this . _resizeObserver = null ;
108- this . _lastCols = 0 ;
109- this . _lastRows = 0 ;
110- this . _suppressPtyResize = false ;
111107 this . _disposed = false ;
112108
113109 // Bound event handlers for cleanup
@@ -157,12 +153,6 @@ define(function (require, exports, module) {
157153 // Fit to container
158154 this . _fit ( ) ;
159155
160- // Use ResizeObserver for reliable resize detection
161- this . _resizeObserver = new ResizeObserver ( ( ) => {
162- this . handleResize ( ) ;
163- } ) ;
164- this . _resizeObserver . observe ( this . $container [ 0 ] ) ;
165-
166156 // Set up custom key handler to intercept editor shortcuts
167157 this . terminal . attachCustomKeyEventHandler ( this . _customKeyHandler . bind ( this ) ) ;
168158
@@ -175,9 +165,9 @@ define(function (require, exports, module) {
175165 }
176166 } ) ;
177167
178- // Wire resize: terminal -> PTY (suppressed during _fit to control timing)
168+ // Wire resize: terminal -> PTY
179169 this . terminal . onResize ( ( { cols, rows} ) => {
180- if ( this . isAlive && ! this . _suppressPtyResize ) {
170+ if ( this . isAlive ) {
181171 this . nodeConnector . execPeer ( "resizeTerminal" , { id : this . id , cols, rows} ) . catch ( ( err ) => {
182172 console . error ( "Terminal: resize error:" , err ) ;
183173 } ) ;
@@ -282,56 +272,70 @@ define(function (require, exports, module) {
282272
283273 /**
284274 * Fit the terminal to its container.
285- * Suppresses the automatic PTY resize during fit() and clears the
286- * prompt area to avoid garbled output caused by readline redrawing
287- * the prompt on top of xterm's reflowed buffer.
288- * Historical output above the prompt is preserved.
275+ *
276+ * Before reflowing, the prompt area is cleared so that stale wrapped
277+ * text does not survive the reflow as a "ghost" line. xterm.js
278+ * defaults to reflowCursorLine: false, meaning the cursor row is
279+ * excluded from reflow. When the terminal widens, a multi-line
280+ * wrapped prompt cannot merge back into one line; the first part
281+ * remains as a visible ghost. By erasing the prompt region first
282+ * (walking up through isWrapped lines), the reflow has nothing
283+ * stale to preserve, and readline's SIGWINCH redraw writes a
284+ * clean prompt at the new width.
289285 */
290286 TerminalInstance . prototype . _fit = function ( ) {
291- if ( this . fitAddon && this . $container && this . $container . is ( ":visible" ) ) {
292- try {
293- const dims = this . fitAddon . proposeDimensions ( ) ;
294- if ( ! dims || ( dims . cols === this . _lastCols && dims . rows === this . _lastRows ) ) {
295- return ;
296- }
287+ if ( ! this . fitAddon || ! this . $container || ! this . $container . is ( ":visible" ) ) {
288+ return ;
289+ }
297290
298- this . _lastCols = dims . cols ;
299- this . _lastRows = dims . rows ;
300-
301- // Suppress automatic PTY resize from onResize handler
302- this . _suppressPtyResize = true ;
303- this . fitAddon . fit ( ) ;
304- this . _suppressPtyResize = false ;
305-
306- if ( this . isAlive ) {
307- // After reflow, clear only the prompt line and below to
308- // remove garbled content from the reflow/SIGWINCH conflict.
309- // Use cursor position after reflow — the cursor sits at the
310- // end of the (possibly garbled) prompt. Clearing from the
311- // start of the cursor row preserves all output above.
312- const cursorY = this . terminal . buffer . active . cursorY ;
313- this . terminal . write ( "\x1b[" + ( cursorY + 1 ) + ";1H\x1b[J" ) ;
314-
315- this . nodeConnector . execPeer ( "resizeTerminal" , {
316- id : this . id , cols : dims . cols , rows : dims . rows
317- } ) . catch ( ( err ) => {
318- console . error ( "Terminal: resize error:" , err ) ;
319- } ) ;
291+ try {
292+ // Clear the prompt region before reflow to prevent ghost lines.
293+ // xterm.js write() is asynchronous, so we must wait for the
294+ // clear to be processed before calling fit().
295+ if ( this . terminal && this . isAlive ) {
296+ const buf = this . terminal . buffer . active ;
297+ let promptStart = buf . cursorY ;
298+
299+ // Walk upward through wrapped lines to find prompt start
300+ while ( promptStart > 0 ) {
301+ const line = buf . getLine ( buf . baseY + promptStart ) ;
302+ if ( ! line || ! line . isWrapped ) {
303+ break ;
304+ }
305+ promptStart -- ;
320306 }
321- } catch ( e ) {
322- // Container might not be visible yet
307+
308+ // Erase from prompt start to end of screen, then fit
309+ // once the erase has been applied to the buffer.
310+ this . terminal . write (
311+ "\x1b[" + ( promptStart + 1 ) + ";1H\x1b[J" ,
312+ ( ) => {
313+ try {
314+ this . fitAddon . fit ( ) ;
315+ } catch ( e ) {
316+ // Container might not be visible yet
317+ }
318+ }
319+ ) ;
320+ return ;
323321 }
322+
323+ this . fitAddon . fit ( ) ;
324+ } catch ( e ) {
325+ // Container might not be visible yet
324326 }
325327 } ;
326328
327329 /**
328- * Handle container resize - debounced
330+ * Handle container resize — debounced so that only the final size
331+ * triggers a reflow + PTY resize. This avoids garbled prompts from
332+ * intermediate SIGWINCH signals during continuous drag-resizing.
329333 */
330334 TerminalInstance . prototype . handleResize = function ( ) {
331335 clearTimeout ( this . _resizeTimeout ) ;
332336 this . _resizeTimeout = setTimeout ( ( ) => {
333337 this . _fit ( ) ;
334- } , 150 ) ;
338+ } , 300 ) ;
335339 } ;
336340
337341 /**
@@ -402,12 +406,8 @@ define(function (require, exports, module) {
402406 this . isAlive = false ;
403407 }
404408
405- // Dispose resize observer and xterm
409+ // Dispose xterm
406410 clearTimeout ( this . _resizeTimeout ) ;
407- if ( this . _resizeObserver ) {
408- this . _resizeObserver . disconnect ( ) ;
409- this . _resizeObserver = null ;
410- }
411411 if ( this . terminal ) {
412412 this . terminal . dispose ( ) ;
413413 this . terminal = null ;
0 commit comments