mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 17:10:43 -10:00
Merge pull request #8156 from SivanCola/fix/tui-scroll-renderer-boundary
Limit legacy TUI scroll clears to Warp / 仅在 Warp 保留 TUI 滚动清屏
This commit is contained in:
4 files changed
+65
-33
No files matched your search
@@ -117,9 +117,9 @@ type chatTUI struct {
|
||||
// marker rides in outgoing user messages so the cache-stable prompt prefix is
|
||||
// left untouched.
|
||||
planMode bool
|
||||
// sessionSwitch is set by replayActiveBranch to suppress the ClearScreen
|
||||
// flicker when the viewport content is completely rebuilt during a session
|
||||
// switch (#5441). Cleared after one Update cycle.
|
||||
// legacyScrollClear keeps the per-offset ClearScreen workaround only for Warp.
|
||||
legacyScrollClear bool
|
||||
// sessionSwitch suppresses that workaround during a transcript rebuild (#5441).
|
||||
sessionSwitch bool
|
||||
// yoloRestoreToolApprovalMode remembers the Ask/Auto base mode that Ctrl+Y
|
||||
// should restore after a desktop-style YOLO toggle.
|
||||
@@ -628,6 +628,7 @@ func newChatTUI(ctrl control.SessionAPI, missing string, eventCh chan event.Even
|
||||
modelRef: ctrl.ModelRef(),
|
||||
missing: missing,
|
||||
nativeScrollback: nativeScrollback,
|
||||
legacyScrollClear: useLegacyViewportScrollClear(runtime.GOOS, os.Environ()),
|
||||
mouseCaptureOff: mouseCaptureOffByDefault(),
|
||||
input: ti,
|
||||
spinner: sp,
|
||||
@@ -976,10 +977,9 @@ func (m chatTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the legacy full redraw where Warp's scroll optimization can strand
|
||||
// stale rows. Bubble Tea already disables that optimization on Windows,
|
||||
// where an extra asynchronous ClearScreen visibly flickers (#8090).
|
||||
if useLegacyViewportScrollClear(runtime.GOOS) && cm.viewport.YOffset() != prevYOff && !cm.nativeScrollback && !cm.sessionSwitch {
|
||||
// Keep the legacy full redraw only where Warp's scroll optimization can
|
||||
// strand stale rows. Every other terminal relies on Bubble Tea's renderer.
|
||||
if cm.legacyScrollClear && cm.viewport.YOffset() != prevYOff && !cm.nativeScrollback && !cm.sessionSwitch {
|
||||
cm.sessionSwitch = false
|
||||
return cm, batchCmds(tea.ClearScreen, mouseCmd, cmd)
|
||||
}
|
||||
|
||||
@@ -1540,10 +1540,14 @@ func TestMouseWheelAndPageKeysScrollTranscript(t *testing.T) {
|
||||
ch := make(chan event.Event, 1)
|
||||
notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"})
|
||||
adv := func(m chatTUI, msg tea.Msg) chatTUI {
|
||||
n, _ := m.Update(msg)
|
||||
n, cmd := m.Update(msg)
|
||||
_, wheel := msg.(tea.MouseWheelMsg)
|
||||
_, key := msg.(tea.KeyPressMsg)
|
||||
if cmd != nil && (wheel || key) {
|
||||
t.Fatalf("viewport update %T should rely on the renderer diff, got command %T", msg, cmd)
|
||||
}
|
||||
return n.(chatTUI)
|
||||
}
|
||||
|
||||
cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 10})
|
||||
for range 40 {
|
||||
cur = adv(cur, notice)
|
||||
@@ -1555,23 +1559,20 @@ func TestMouseWheelAndPageKeysScrollTranscript(t *testing.T) {
|
||||
if bottom <= cur.viewport.Height()+3 {
|
||||
t.Fatalf("test transcript did not overflow enough: bottom=%d height=%d", bottom, cur.viewport.Height())
|
||||
}
|
||||
|
||||
cur.legacyScrollClear = false
|
||||
cur = adv(cur, tea.MouseWheelMsg{Button: tea.MouseWheelUp})
|
||||
if got, want := cur.viewport.YOffset(), bottom-3; got != want {
|
||||
t.Fatalf("wheel-up YOffset = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
cur = adv(cur, tea.MouseWheelMsg{Button: tea.MouseWheelDown})
|
||||
if got := cur.viewport.YOffset(); got != bottom {
|
||||
t.Fatalf("wheel-down should return by one wheel step, YOffset=%d want bottom=%d", got, bottom)
|
||||
}
|
||||
|
||||
cur = adv(cur, tea.KeyPressMsg{Code: tea.KeyPgUp})
|
||||
pageUp := cur.viewport.YOffset()
|
||||
if got, want := pageUp, bottom-cur.viewport.Height(); got != want {
|
||||
t.Fatalf("PageUp YOffset = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
cur = adv(cur, tea.KeyPressMsg{Code: tea.KeyPgDown})
|
||||
if got := cur.viewport.YOffset(); got != bottom {
|
||||
t.Fatalf("PageDown should return to bottom from one page up, YOffset=%d want %d", got, bottom)
|
||||
@@ -3103,7 +3104,6 @@ func TestForceGotoBottomScrollsWithoutTranscriptChange(t *testing.T) {
|
||||
n, _ := adv(m, msg)
|
||||
return n
|
||||
}
|
||||
|
||||
cur := next(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8})
|
||||
for range 12 {
|
||||
cur = next(cur, notice)
|
||||
@@ -3119,6 +3119,7 @@ func TestForceGotoBottomScrollsWithoutTranscriptChange(t *testing.T) {
|
||||
|
||||
cur.forceGotoBottom = true
|
||||
cur.transcriptDirty = false
|
||||
cur.legacyScrollClear = false
|
||||
cur, cmd := adv(cur, tea.WindowSizeMsg{Width: 80, Height: 8})
|
||||
|
||||
if !cur.viewport.AtBottom() {
|
||||
@@ -3127,10 +3128,10 @@ func TestForceGotoBottomScrollsWithoutTranscriptChange(t *testing.T) {
|
||||
if cur.forceGotoBottom {
|
||||
t.Fatal("forceGotoBottom should be cleared after scrolling")
|
||||
}
|
||||
assertLegacyViewportClearCmd(t, cmd)
|
||||
assertLegacyViewportClearCmd(t, cmd, false)
|
||||
}
|
||||
|
||||
func TestSessionSwitchSuppressesOneClearScreen(t *testing.T) {
|
||||
func TestSessionSwitchSuppressesOneWarpClearScreen(t *testing.T) {
|
||||
ctrl := control.New(control.Options{})
|
||||
ch := make(chan event.Event, 1)
|
||||
notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"})
|
||||
@@ -3151,14 +3152,13 @@ func TestSessionSwitchSuppressesOneClearScreen(t *testing.T) {
|
||||
if cur.viewport.AtBottom() {
|
||||
t.Fatal("wheel-up should break the bottom pin")
|
||||
}
|
||||
|
||||
cur.legacyScrollClear = true
|
||||
cur.sessionSwitch = true
|
||||
cur.forceGotoBottom = true
|
||||
cur.transcriptDirty = false
|
||||
cur, cmd := adv(cur, tea.WindowSizeMsg{Width: 80, Height: 8})
|
||||
|
||||
if cmd != nil {
|
||||
t.Fatal("session switch rebuild should suppress the ClearScreen scroll-jump workaround once")
|
||||
t.Fatal("session switch rebuild should suppress the Warp ClearScreen workaround once")
|
||||
}
|
||||
if cur.sessionSwitch {
|
||||
t.Fatal("sessionSwitch should be cleared after one Update")
|
||||
@@ -3170,7 +3170,7 @@ func TestSessionSwitchSuppressesOneClearScreen(t *testing.T) {
|
||||
cur = next(cur, tea.MouseWheelMsg{Button: tea.MouseWheelUp})
|
||||
cur.forceGotoBottom = true
|
||||
cur, cmd = adv(cur, tea.WindowSizeMsg{Width: 80, Height: 8})
|
||||
assertLegacyViewportClearCmd(t, cmd)
|
||||
assertLegacyViewportClearCmd(t, cmd, true)
|
||||
if cur.sessionSwitch {
|
||||
t.Fatal("sessionSwitch should remain false after the suppressed cycle")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cli
|
||||
|
||||
import "strings"
|
||||
|
||||
// scrollFollowMode is an explicit transcript tail-follow state machine.
|
||||
//
|
||||
// Previously the post-Update path used a single wasAtBottom snapshot taken
|
||||
@@ -66,6 +68,20 @@ func (m *chatTUI) syncScrollModeAfterGesture() {
|
||||
m.markUserScrolled()
|
||||
}
|
||||
|
||||
// useLegacyViewportScrollClear excludes Windows, where Bubble Tea already
|
||||
// disables the scroll optimization and an extra ClearScreen flickers (#8090).
|
||||
func useLegacyViewportScrollClear(goos string) bool { return goos != "windows" }
|
||||
// useLegacyViewportScrollClear limits the application-level redraw workaround
|
||||
// to Warp, where Bubble Tea's scroll optimization can strand stale rows. It is
|
||||
// disabled on Windows even if Warp identifies itself there because Bubble Tea
|
||||
// already avoids the incompatible optimization on that platform, and an extra
|
||||
// asynchronous ClearScreen visibly flickers (#8090).
|
||||
func useLegacyViewportScrollClear(goos string, environ []string) bool {
|
||||
if goos == "windows" {
|
||||
return false
|
||||
}
|
||||
for _, entry := range environ {
|
||||
key, value, ok := strings.Cut(entry, "=")
|
||||
if ok && key == "TERM_PROGRAM" && strings.EqualFold(strings.TrimSpace(value), "WarpTerminal") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -15,19 +14,36 @@ import (
|
||||
|
||||
func TestUseLegacyViewportScrollClear(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
goos string
|
||||
want bool
|
||||
}{{"windows", false}, {"linux", true}, {"darwin", true}} {
|
||||
if got := useLegacyViewportScrollClear(tt.goos); got != tt.want {
|
||||
t.Errorf("useLegacyViewportScrollClear(%q) = %v, want %v", tt.goos, got, tt.want)
|
||||
}
|
||||
name string
|
||||
goos string
|
||||
environ []string
|
||||
want bool
|
||||
}{
|
||||
{name: "windows", goos: "windows", environ: []string{"TERM_PROGRAM=Windows_Terminal"}},
|
||||
{name: "windows Warp", goos: "windows", environ: []string{"TERM_PROGRAM=WarpTerminal"}},
|
||||
{name: "macOS Warp", goos: "darwin", environ: []string{"TERM_PROGRAM=WarpTerminal"}, want: true},
|
||||
{name: "Linux Warp case and whitespace", goos: "linux", environ: []string{"TERM_PROGRAM= warPterminal "}, want: true},
|
||||
{name: "SSH forwarded Warp", goos: "linux", environ: []string{"SSH_CONNECTION=client", "TERM_PROGRAM=WarpTerminal"}, want: true},
|
||||
{name: "Apple Terminal", goos: "darwin", environ: []string{"TERM_PROGRAM=Apple_Terminal"}},
|
||||
{name: "iTerm", goos: "darwin", environ: []string{"TERM_PROGRAM=iTerm.app"}},
|
||||
{name: "Ghostty", goos: "darwin", environ: []string{"TERM_PROGRAM=ghostty"}},
|
||||
{name: "Kitty", goos: "linux", environ: []string{"TERM=xterm-kitty"}},
|
||||
{name: "empty environment", goos: "linux"},
|
||||
{name: "lookalike variable", goos: "linux", environ: []string{"OTHER_TERM_PROGRAM=WarpTerminal"}},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := useLegacyViewportScrollClear(tt.goos, tt.environ)
|
||||
if got != tt.want {
|
||||
t.Fatalf("useLegacyViewportScrollClear(%q, %q) = %v, want %v", tt.goos, tt.environ, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertLegacyViewportClearCmd(t *testing.T, cmd tea.Cmd) {
|
||||
func assertLegacyViewportClearCmd(t *testing.T, cmd tea.Cmd, want bool) {
|
||||
t.Helper()
|
||||
if got, want := cmd != nil, useLegacyViewportScrollClear(runtime.GOOS); got != want {
|
||||
t.Fatalf("viewport ClearScreen command = %v, want %v on %s", got, want, runtime.GOOS)
|
||||
if got := cmd != nil; got != want {
|
||||
t.Fatalf("viewport ClearScreen command = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user