@ -163,15 +163,18 @@
const PAGE _LINES = 500 ;
let scrollbackLines = 0 ; // 0 = live view
let inScrollMode = false ;
let scrollbackPending = false ;
function requestScrollback ( lines ) {
if ( ! currentWorkspace || ! currentSurface ) return ;
scrollbackPending = true ;
ws . requestScroll ( currentWorkspace , currentSurface , lines ) ;
}
function exitScrollMode ( ) {
inScrollMode = false ;
scrollbackLines = 0 ;
scrollbackPending = false ;
terminal . autoScroll = true ;
// Re-subscribe to get fresh live content
if ( currentWorkspace && currentSurface ) {
@ -179,6 +182,16 @@
}
}
// Auto-load scrollback when user scrolls to the top
terminalContainer . addEventListener ( 'scroll' , ( ) => {
if ( scrollbackPending ) return ;
if ( terminalContainer . scrollTop < 50 && currentWorkspace && currentSurface ) {
inScrollMode = true ;
scrollbackLines += PAGE _LINES ;
requestScrollback ( scrollbackLines ) ;
}
} ) ;
const keyboard = new VirtualKeyboard (
( text ) => {
if ( currentWorkspace && currentSurface ) {
@ -337,8 +350,11 @@
} ) ;
ws . on ( 'auth-ok' , ( ) => {
// Request workspace list after auth
ws . listWorkspaces ( ) ;
// Re-subscribe to current surface after reconnect
if ( currentWorkspace && currentSurface ) {
ws . subscribe ( currentWorkspace , currentSurface ) ;
}
} ) ;
ws . on ( 'workspaces' , ( msg ) => {
@ -378,16 +394,24 @@
ws . on ( 'screen' , ( msg ) => {
if ( msg . surface !== currentSurface ) return ;
if ( inScrollMode && ! msg . scrollback ) return ;
terminal . setContent ( msg . lines ) ;
if ( msg . scrollback ) {
// Preserve scroll position relative to bottom so content doesn't jump
const prevScrollHeight = terminalContainer . scrollHeight ;
const prevScrollTop = terminalContainer . scrollTop ;
terminal . setContent ( msg . lines ) ;
terminal . autoScroll = false ;
// After render, scroll to show content just before current view (most recently scrolled-off)
requestAnimationFrame ( ( ) => {
scrollbackPending = false ;
requestAnimationFrame ( ( ) => {
terminalContainer . scrollTop = terminalContainer . scrollHeight - terminalContainer . clientHeight ;
} ) ;
const newScrollHeight = terminalContainer . scrollHeight ;
const added = newScrollHeight - prevScrollHeight ;
// Keep the same content visible — shift scroll by the amount of new content added above
terminalContainer . scrollTop = prevScrollTop + Math . max ( added , 0 ) ;
} ) ;
} else {
terminal . setContent ( msg . lines ) ;
}
} ) ;
@ -481,12 +505,22 @@
}
} ) ;
// Visibility API: pause polling when hidden
// Visibility API: pause polling when hidden, reconnect when visible
let hiddenSince = 0 ;
document . addEventListener ( 'visibilitychange' , ( ) => {
if ( document . hidden ) {
hiddenSince = Date . now ( ) ;
if ( currentSurface ) ws . unsubscribe ( currentSurface ) ;
} else {
if ( currentWorkspace && currentSurface ) {
const elapsed = hiddenSince ? Date . now ( ) - hiddenSince : 0 ;
hiddenSince = 0 ;
if ( ! currentWorkspace || ! currentSurface ) return ;
// iOS kills WebSocket connections in background.
// If hidden >5s or connection is dead, force fresh reconnect.
if ( ! ws . isConnected ( ) || elapsed > 5000 ) {
ws . reconnectNow ( ) ;
} else {
ws . subscribe ( currentWorkspace , currentSurface ) ;
}
}