@@ -15,20 +15,31 @@ func (m *Model) flushTiming(tab *Tab, active bool) (time.Duration, time.Duration
1515 quiet := ptyFlushQuiet
1616 maxInterval := ptyFlushMaxInterval
1717
18+ // Snapshot terminal state under lock, then release before the load-sampling call.
1819 tab .mu .Lock ()
20+ altScreen := tab .Terminal != nil && tab .Terminal .AltScreen
21+ var termWidth , termHeight int
22+ var pendingLen int
23+ if tab .Terminal != nil {
24+ termWidth = tab .Terminal .Width
25+ termHeight = tab .Terminal .Height
26+ pendingLen = len (tab .pendingOutput )
27+ }
28+ tab .mu .Unlock ()
29+
1930 // Only use slower Alt timing for true AltScreen mode (full-screen TUIs).
2031 // SyncActive (DEC 2026) already handles partial updates via screen snapshots,
2132 // so we don't need slower flush timing - it just makes streaming text feel laggy.
22- if tab . Terminal != nil && tab . Terminal . AltScreen {
33+ if altScreen {
2334 quiet = ptyFlushQuietAlt
2435 maxInterval = ptyFlushMaxAlt
2536 }
2637
2738 // Apply backpressure when pending output exceeds threshold
2839 // This prevents renderer thrashing during heavy output (like builds)
29- if tab . Terminal != nil && len ( tab . pendingOutput ) > 0 {
30- threshold := ptyBackpressureMultiplier * tab . Terminal . Width * tab . Terminal . Height
31- if len ( tab . pendingOutput ) > threshold {
40+ if pendingLen > 0 {
41+ threshold := ptyBackpressureMultiplier * termWidth * termHeight
42+ if pendingLen > threshold {
3243 // Under backpressure: use minimum flush interval
3344 if quiet < ptyBackpressureFlushFloor {
3445 quiet = ptyBackpressureFlushFloor
@@ -38,11 +49,26 @@ func (m *Model) flushTiming(tab *Tab, active bool) (time.Duration, time.Duration
3849 }
3950 }
4051 }
41- tab .mu .Unlock ()
4252
4353 if ! active {
44- quiet *= ptyFlushInactiveMultiplier
45- maxInterval *= ptyFlushInactiveMultiplier
54+ busyCount := m .busyPTYTabCount (time .Now ())
55+ var mult time.Duration
56+ switch {
57+ case busyCount >= ptyVeryHeavyLoadTabThreshold :
58+ mult = ptyFlushInactiveVeryHeavyMultiplier
59+ case busyCount >= ptyHeavyLoadTabThreshold :
60+ mult = ptyFlushInactiveHeavyMultiplier
61+ default :
62+ mult = ptyFlushInactiveMultiplier
63+ }
64+ quiet *= mult
65+ maxInterval *= mult
66+ if quiet > ptyFlushInactiveMaxIntervalCap {
67+ quiet = ptyFlushInactiveMaxIntervalCap
68+ }
69+ if maxInterval > ptyFlushInactiveMaxIntervalCap {
70+ maxInterval = ptyFlushInactiveMaxIntervalCap
71+ }
4672 if maxInterval < quiet {
4773 maxInterval = quiet
4874 }
@@ -51,6 +77,28 @@ func (m *Model) flushTiming(tab *Tab, active bool) (time.Duration, time.Duration
5177 return quiet , maxInterval
5278}
5379
80+ func (m * Model ) busyPTYTabCount (now time.Time ) int {
81+ // Return cached count if sampled within ptyLoadSampleInterval (100ms)
82+ if ! m .flushLoadSampleAt .IsZero () && now .Sub (m .flushLoadSampleAt ) < ptyLoadSampleInterval {
83+ return m .cachedBusyTabCount
84+ }
85+ count := 0
86+ for _ , tabs := range m .tabsByWorkspace {
87+ for _ , tab := range tabs {
88+ if tab == nil || tab .isClosed () {
89+ continue
90+ }
91+ busy := atomic .LoadUint32 (& tab .readerActiveState ) == 1 || len (tab .pendingOutput ) > 0
92+ if busy {
93+ count ++
94+ }
95+ }
96+ }
97+ m .flushLoadSampleAt = now
98+ m .cachedBusyTabCount = count
99+ return count
100+ }
101+
54102func (m * Model ) forwardPTYMsgs (msgCh <- chan tea.Msg ) {
55103 for msg := range msgCh {
56104 if msg == nil {
0 commit comments