native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #51 from onevcat/fix/terminal-scrollback-autofollow

Fix terminal auto-follow while reading scrollback

authored by

Wei Wang and committed by
GitHub
b4a08c3e 2626e871

+25 -15
+25 -15
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 1671 1671 private let documentView: NSView 1672 1672 private let surfaceView: GhosttySurfaceView 1673 1673 private var observers: [NSObjectProtocol] = [] 1674 - private static let logger = SupaLogger("ScrollView") 1675 1674 1676 1675 private var isLiveScrolling = false 1676 + private var isProgrammaticScrollChange = false 1677 + private var isUserScrolledBack = false 1677 1678 private var lastSentRow: Int? 1678 1679 private var scrollbar: ScrollbarState? 1679 1680 ··· 1730 1731 ) { [weak self] _ in 1731 1732 MainActor.assumeIsolated { 1732 1733 self?.isLiveScrolling = false 1734 + self?.updateScrollBackState() 1733 1735 } 1734 1736 }) 1735 1737 ··· 1805 1807 1806 1808 private func handleScrollChange() { 1807 1809 synchronizeSurfaceView() 1810 + guard !isProgrammaticScrollChange else { 1811 + return 1812 + } 1813 + updateScrollBackState() 1808 1814 } 1809 1815 1810 1816 private func handleScrollerStyleChange() { ··· 1819 1825 1820 1826 private func synchronizeScrollView() { 1821 1827 documentView.frame.size.height = documentHeight() 1822 - if !isLiveScrolling { 1828 + if !isLiveScrolling && !isUserScrolledBack { 1823 1829 let cellHeight = surfaceView.currentCellSize().height 1824 1830 if cellHeight > 0, let scrollbar { 1825 - // Log when a scrollbar update would jump the viewport significantly, 1826 - // which is the suspected auto-scroll-to-bottom symptom. 1827 - let visibleRect = scrollView.contentView.documentVisibleRect 1828 - let currentY = visibleRect.origin.y 1829 1831 let targetY = 1830 1832 CGFloat(scrollbar.total - scrollbar.offset - scrollbar.length) * cellHeight 1831 - let jump = abs(targetY - currentY) 1832 - if jump > cellHeight * 2 { 1833 - Self.logger.warning( 1834 - "Scroll jump detected: currentY=\(currentY) targetY=\(targetY) " 1835 - + "jump=\(jump) scrollbar=(total:\(scrollbar.total) offset:\(scrollbar.offset) " 1836 - + "length:\(scrollbar.length))" 1837 - ) 1838 - } 1839 - 1833 + isProgrammaticScrollChange = true 1834 + defer { isProgrammaticScrollChange = false } 1840 1835 scrollView.contentView.scroll(to: CGPoint(x: 0, y: targetY)) 1841 1836 lastSentRow = Int(scrollbar.offset) 1842 1837 } 1843 1838 } 1844 1839 scrollView.reflectScrolledClipView(scrollView.contentView) 1840 + } 1841 + 1842 + /// Tracks whether the user intentionally moved away from the live bottom of 1843 + /// the terminal. While this is true we keep the viewport fixed so incoming 1844 + /// output cannot yank scrollback out from under the user. 1845 + private func updateScrollBackState() { 1846 + let cellHeight = surfaceView.currentCellSize().height 1847 + guard cellHeight > 0 else { 1848 + isUserScrolledBack = false 1849 + return 1850 + } 1851 + 1852 + let visibleRect = scrollView.contentView.documentVisibleRect 1853 + let distanceFromBottom = max(0, documentView.frame.height - visibleRect.maxY) 1854 + isUserScrolledBack = distanceFromBottom > cellHeight / 2 1845 1855 } 1846 1856 1847 1857 private func handleLiveScroll() {