@@ -179,6 +179,7 @@ Object.assign(CodemanApp.prototype, {
179179 // Accumulate sub-line pixel deltas so slow swipes still scroll
180180 let pixelAccum = 0 ;
181181
182+ let didScroll = false ; // track whether touchmove fired (tap vs scroll)
182183 container . addEventListener (
183184 'touchstart' ,
184185 ( ev ) => {
@@ -187,6 +188,7 @@ Object.assign(CodemanApp.prototype, {
187188 velocity = 0 ;
188189 pixelAccum = 0 ;
189190 isTouching = true ;
191+ didScroll = false ;
190192 lastTime = 0 ;
191193 if ( scrollFrame ) {
192194 cancelAnimationFrame ( scrollFrame ) ;
@@ -201,6 +203,7 @@ Object.assign(CodemanApp.prototype, {
201203 'touchmove' ,
202204 ( ev ) => {
203205 if ( ev . touches . length === 1 && isTouching ) {
206+ didScroll = true ;
204207 const touchY = ev . touches [ 0 ] . clientY ;
205208 const delta = touchLastY - touchY ; // positive = scroll down
206209 pixelAccum += delta ;
@@ -225,6 +228,12 @@ Object.assign(CodemanApp.prototype, {
225228 if ( ! scrollFrame && Math . abs ( velocity ) > 0.3 ) {
226229 scrollFrame = requestAnimationFrame ( scrollLoop ) ;
227230 }
231+ // Tap (no scroll): refocus xterm's hidden textarea so keyboard input
232+ // routes back to the terminal. Without this, a tap on the terminal area
233+ // consumes the touch event but xterm's textarea never regains focus.
234+ if ( ! didScroll && this . terminal ) {
235+ this . terminal . focus ( ) ;
236+ }
228237 } ,
229238 { passive : true }
230239 ) ;
@@ -284,22 +293,6 @@ Object.assign(CodemanApp.prototype, {
284293 }
285294 this . flushFlickerBuffer ( ) ;
286295 }
287- // Clear viewport + scrollback for Ink-based sessions before sending SIGWINCH.
288- // fitAddon.fit() reflows content: lines at old width may wrap to more rows,
289- // pushing overflow into scrollback. Ink's cursor-up count is based on the
290- // pre-reflow line count, so ghost renders accumulate in scrollback.
291- // Fix: \x1b[3J (Erase Saved Lines) clears scrollback reflow debris,
292- // then \x1b[H\x1b[2J clears the viewport for a clean Ink redraw.
293- const activeResizeSession = this . activeSessionId ? this . sessions . get ( this . activeSessionId ) : null ;
294- if (
295- activeResizeSession &&
296- activeResizeSession . mode !== 'shell' &&
297- ! activeResizeSession . _ended &&
298- this . terminal &&
299- this . isTerminalAtBottom ( )
300- ) {
301- this . terminal . write ( '\x1b[3J\x1b[H\x1b[2J' ) ;
302- }
303296 // Skip server resize while mobile keyboard is visible — sending SIGWINCH
304297 // causes Ink to re-render at the new row count, garbling terminal output.
305298 // Local fit() still runs so xterm knows the viewport size for scrolling.
@@ -311,6 +304,24 @@ Object.assign(CodemanApp.prototype, {
311304 const rows = dims ? Math . max ( dims . rows , MIN_ROWS ) : MIN_ROWS ;
312305 // Only send resize if dimensions actually changed
313306 if ( ! this . _lastResizeDims || cols !== this . _lastResizeDims . cols || rows !== this . _lastResizeDims . rows ) {
307+ // Clear viewport + scrollback ONLY when dimensions actually change.
308+ // fitAddon.fit() reflows content: lines at old width may wrap to more rows,
309+ // pushing overflow into scrollback. Ink's cursor-up count is based on the
310+ // pre-reflow line count, so ghost renders accumulate in scrollback.
311+ // Fix: \x1b[3J (Erase Saved Lines) clears scrollback reflow debris,
312+ // then \x1b[H\x1b[2J clears the viewport for a clean Ink redraw.
313+ // IMPORTANT: Only clear when we're actually sending SIGWINCH (dims changed).
314+ // Clearing without a subsequent Ink redraw leaves the terminal blank.
315+ const activeResizeSession = this . activeSessionId ? this . sessions . get ( this . activeSessionId ) : null ;
316+ if (
317+ activeResizeSession &&
318+ activeResizeSession . mode !== 'shell' &&
319+ ! activeResizeSession . _ended &&
320+ this . terminal &&
321+ this . isTerminalAtBottom ( )
322+ ) {
323+ this . terminal . write ( '\x1b[3J\x1b[H\x1b[2J' ) ;
324+ }
314325 this . _lastResizeDims = { cols, rows } ;
315326 fetch ( `/api/sessions/${ this . activeSessionId } /resize` , {
316327 method : 'POST' ,
@@ -1348,6 +1359,10 @@ Object.assign(CodemanApp.prototype, {
13481359 if ( this . fitAddon ) this . fitAddon . fit ( ) ;
13491360 const dims = this . getTerminalDimensions ( ) ;
13501361 if ( ! dims ) return ;
1362+ // Update _lastResizeDims so the throttledResize handler won't redundantly
1363+ // clear the terminal for the same dimensions (which would blank the screen
1364+ // without a subsequent Ink redraw to repaint it).
1365+ this . _lastResizeDims = { cols : dims . cols , rows : dims . rows } ;
13511366 // Fast path: WebSocket resize
13521367 if ( this . _wsReady && this . _wsSessionId === sessionId ) {
13531368 try {
0 commit comments