@@ -618,15 +618,14 @@ class CodemanApp {
618618 const container = document . getElementById ( 'terminalContainer' ) ;
619619 this . terminal . open ( container ) ;
620620
621- // Activate WebGL renderer for up to 900% faster rendering (fallback to canvas on failure).
622- // Store reference so we can disable during large buffer loads to prevent GPU stalls.
623- // Disable with ?nowebgl=1 URL param for debugging GPU freeze issues.
621+ // WebGL renderer disabled — canvas renderer used instead.
622+ // xterm.js WebGL addon causes synchronous GPU ReadPixels calls during large
623+ // terminal writes (buffer loads, heavy Ink output) that block Chrome's main
624+ // thread for 10+ seconds, triggering "page unresponsive" crashes.
625+ // Canvas renderer handles the same workloads without GPU stalls.
626+ // Re-enable with ?webgl=1 URL param for testing.
624627 this . _webglAddon = null ;
625- const _noWebGL = new URLSearchParams ( location . search ) . has ( 'nowebgl' ) ;
626- if ( _noWebGL ) {
627- console . warn ( '[CRASH-DIAG] WebGL renderer DISABLED via ?nowebgl param — using canvas renderer' ) ;
628- }
629- if ( ! _noWebGL && typeof WebglAddon !== 'undefined' ) {
628+ if ( new URLSearchParams ( location . search ) . has ( 'webgl' ) && typeof WebglAddon !== 'undefined' ) {
630629 try {
631630 this . _webglAddon = new WebglAddon . WebglAddon ( ) ;
632631 this . _webglAddon . onContextLoss ( ( ) => {
@@ -635,6 +634,7 @@ class CodemanApp {
635634 this . _webglAddon = null ;
636635 } ) ;
637636 this . terminal . loadAddon ( this . _webglAddon ) ;
637+ console . log ( '[CRASH-DIAG] WebGL renderer enabled via ?webgl param' ) ;
638638 } catch ( _e ) { /* WebGL2 unavailable — canvas renderer used */ }
639639 }
640640
@@ -1376,11 +1376,13 @@ class CodemanApp {
13761376 // This implements synchronized output for xterm.js which doesn't support DEC 2026 natively
13771377 const _joinedLen = this . pendingWrites . reduce ( ( s , w ) => s + w . length , 0 ) ;
13781378 if ( _joinedLen > 16384 ) _crashDiag . log ( `FLUSH: ${ ( _joinedLen / 1024 ) . toFixed ( 0 ) } KB` ) ;
1379- const segments = extractSyncSegments ( this . pendingWrites . join ( '' ) ) ;
1379+ const joined = this . pendingWrites . join ( '' ) ;
13801380 this . pendingWrites = [ ] ;
13811381
1382- // Write all segments in a single batch (atomic within this frame)
1383- // xterm.js internally batches multiple write() calls within same frame
1382+ const segments = extractSyncSegments ( joined ) ;
1383+
1384+ // Write all segments in a single batch (atomic within this frame).
1385+ // xterm.js internally batches multiple write() calls within same frame.
13841386 // Never discard content from incomplete sync blocks — xterm.js doesn't support
13851387 // DEC 2026 natively anyway, so strip the marker and write content regardless.
13861388 // Discarding causes real data loss (including Ink's erase-line escapes).
@@ -1393,7 +1395,7 @@ class CodemanApp {
13931395 }
13941396 }
13951397 const _dt = performance . now ( ) - _t0 ;
1396- if ( _dt > 100 ) console . warn ( `[CRASH-DIAG] flushPendingWrites took ${ _dt . toFixed ( 0 ) } ms (${ _joinedLen } bytes, ${ segments . length } segments )` ) ;
1398+ if ( _dt > 100 ) console . warn ( `[CRASH-DIAG] flushPendingWrites took ${ _dt . toFixed ( 0 ) } ms (${ _joinedLen } bytes)` ) ;
13971399
13981400 // Sticky scroll: if user was at bottom, keep them there after new output
13991401 if ( this . _wasAtBottomBeforeWrite ) {
@@ -3578,6 +3580,12 @@ class CodemanApp {
35783580 // filter catches most cases, but _restoringFlushedState provides a
35793581 // belt-and-suspenders guard for any edge cases.
35803582 this . _restoringFlushedState = true ;
3583+ // Gate live SSE terminal writes for the ENTIRE buffer load sequence.
3584+ // Without this, SSE events arriving during the fetch() gap compete with
3585+ // the buffer write, causing 70KB+ single-frame flushes that stall WebGL.
3586+ // chunkedTerminalWrite also sets this, but we need it before the fetch too.
3587+ this . _isLoadingBuffer = true ;
3588+ this . _loadBufferQueue = [ ] ;
35813589 try {
35823590 // Instant cache restore — show previous buffer via chunked write to avoid WebGL GPU stalls.
35833591 // Direct terminal.write() of large cached buffers (256KB+) can block the main thread
@@ -3588,15 +3596,15 @@ class CodemanApp {
35883596 this . terminal . clear ( ) ;
35893597 this . terminal . reset ( ) ;
35903598 await this . chunkedTerminalWrite ( cachedBuffer ) ;
3591- if ( selectGen !== this . _selectGeneration ) { this . _restoringFlushedState = false ; return ; }
3599+ if ( selectGen !== this . _selectGeneration ) { if ( this . _isLoadingBuffer ) this . _finishBufferLoad ( ) ; this . _restoringFlushedState = false ; return ; }
35923600 this . terminal . scrollToBottom ( ) ;
35933601 _crashDiag . log ( 'CACHE_DONE' ) ;
35943602 }
35953603
35963604 _crashDiag . log ( 'FETCH_START' ) ;
35973605 const tailSize = 256 * 1024 ;
35983606 const res = await fetch ( `/api/sessions/${ sessionId } /terminal?tail=${ tailSize } ` ) ;
3599- if ( selectGen !== this . _selectGeneration ) { this . _restoringFlushedState = false ; return ; }
3607+ if ( selectGen !== this . _selectGeneration ) { if ( this . _isLoadingBuffer ) this . _finishBufferLoad ( ) ; this . _restoringFlushedState = false ; return ; }
36003608 const data = await res . json ( ) ;
36013609 _crashDiag . log ( `FETCH_DONE: ${ data . terminalBuffer ? ( data . terminalBuffer . length / 1024 ) . toFixed ( 0 ) + 'KB' : 'empty' } truncated=${ data . truncated } ` ) ;
36023610
@@ -3615,7 +3623,7 @@ class CodemanApp {
36153623 }
36163624 // Use chunked write for large buffers to avoid UI jank
36173625 await this . chunkedTerminalWrite ( data . terminalBuffer ) ;
3618- if ( selectGen !== this . _selectGeneration ) { this . _restoringFlushedState = false ; return ; }
3626+ if ( selectGen !== this . _selectGeneration ) { if ( this . _isLoadingBuffer ) this . _finishBufferLoad ( ) ; this . _restoringFlushedState = false ; return ; }
36193627 // Ensure terminal is scrolled to bottom after buffer load
36203628 this . terminal . scrollToBottom ( ) ;
36213629 }
@@ -3633,7 +3641,13 @@ class CodemanApp {
36333641 this . terminal . reset ( ) ;
36343642 }
36353643
3636- // Buffer load complete — drop the guard so user input clears state normally
3644+ // Buffer load complete — unblock live SSE writes and flush any queued events.
3645+ // chunkedTerminalWrite calls _finishBufferLoad internally, but if we skipped
3646+ // the chunked write (small buffer, cache hit, or empty), we must call it here.
3647+ if ( this . _isLoadingBuffer ) {
3648+ this . _finishBufferLoad ( ) ;
3649+ }
3650+ // Drop the guard so user input clears state normally
36373651 this . _restoringFlushedState = false ;
36383652
36393653 // Restore flushed offset and text for this session so the overlay positions
@@ -3739,6 +3753,7 @@ class CodemanApp {
37393753 _crashDiag . log ( `SELECT_DONE: ${ ( performance . now ( ) - _selStart ) . toFixed ( 0 ) } ms` ) ;
37403754 console . log ( `[CRASH-DIAG] selectSession DONE: ${ sessionId . slice ( 0 , 8 ) } in ${ ( performance . now ( ) - _selStart ) . toFixed ( 0 ) } ms` ) ;
37413755 } catch ( err ) {
3756+ if ( this . _isLoadingBuffer ) this . _finishBufferLoad ( ) ;
37423757 this . _restoringFlushedState = false ;
37433758 console . error ( 'Failed to load session terminal:' , err ) ;
37443759 }
0 commit comments