tui: drop cursor-home from clear-screen to stop vscode scrollbar jump
- commit
- 33a865b7824fca440469e93ff7db9c561ff1a232
- author
- patriceckhart · · 2026-05-11 07:27 UTC
- parent
- ce78a49
- signature
- unsigned
3 files · +14 -4
M internal/agent/modes/interactive.go +1 -1
| @@ -415,7 +415,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||
| 415 | 415 | // enter the alternate-screen buffer (CSI ?1049h). The renderer emits | |
| 416 | 416 | // chat as normal terminal flow/scrollback and redraws only the live | |
| 417 | 417 | // input/status block on normal typing. | |
| 418 | - | _, _ = term.Write([]byte(tui.SeqBracketedPasteOn + tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqClearScreen + tui.SeqClearScrollback + tui.MoveTo(1, 1))) | |
| 418 | + | _, _ = term.Write([]byte(tui.SeqBracketedPasteOn + tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqClearScreenNoHome + tui.SeqClearScrollback + tui.MoveTo(1, 1))) | |
| 419 | 419 | defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqBracketedPasteOff + tui.SeqShowCursor)) | |
| 420 | 420 | ||
| 421 | 421 | // Streaming pacer: drains buffered text deltas at a steady rate | |
| @@ -415,7 +415,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||||
| 415 | // enter the alternate-screen buffer (CSI ?1049h). The renderer emits | 415 | // enter the alternate-screen buffer (CSI ?1049h). The renderer emits | ||
| 416 | // chat as normal terminal flow/scrollback and redraws only the live | 416 | // chat as normal terminal flow/scrollback and redraws only the live | ||
| 417 | // input/status block on normal typing. | 417 | // input/status block on normal typing. | ||
| 418 | - | _, _ = term.Write([]byte(tui.SeqBracketedPasteOn + tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqClearScreen + tui.SeqClearScrollback + tui.MoveTo(1, 1))) | 418 | + | _, _ = term.Write([]byte(tui.SeqBracketedPasteOn + tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqClearScreenNoHome + tui.SeqClearScrollback + tui.MoveTo(1, 1))) |
| 419 | defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqBracketedPasteOff + tui.SeqShowCursor)) | 419 | defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqBracketedPasteOff + tui.SeqShowCursor)) | ||
| 420 | 420 | ||||
| 421 | // Streaming pacer: drains buffered text deltas at a steady rate | 421 | // Streaming pacer: drains buffered text deltas at a steady rate | ||
M internal/tui/render.go +5 -3
| @@ -79,7 +79,9 @@ func (r *Renderer) Resize(cols, rows int) { | |||
| 79 | 79 | // Clear both screen and scrollback so stale content from | |
| 80 | 80 | // the old width doesn't bleed through. Move to (1,1) so | |
| 81 | 81 | // the next DrawLog/writeFull starts from a clean slate. | |
| 82 | - | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreen+SeqClearScrollback+MoveTo(1, 1)) | |
| 82 | + | // Use the no-home variant: the explicit MoveTo below sets | |
| 83 | + | // the cursor without triggering VS Code's viewport-snap. | |
| 84 | + | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreenNoHome+SeqClearScrollback+MoveTo(1, 1)) | |
| 83 | 85 | } | |
| 84 | 86 | } | |
| 85 | 87 | } | |
| @@ -97,7 +99,7 @@ func (r *Renderer) Clear() { | |||
| 97 | 99 | r.logViewportTop = 0 | |
| 98 | 100 | r.logHardwareRow = 0 | |
| 99 | 101 | r.logInit = false | |
| 100 | - | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreen+SeqClearScrollback+MoveTo(1, 1)) | |
| 102 | + | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreenNoHome+SeqClearScrollback+MoveTo(1, 1)) | |
| 101 | 103 | } | |
| 102 | 104 | ||
| 103 | 105 | // Invalidate forces a full repaint on the next Draw without clearing the | |
| @@ -334,7 +336,7 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int | |||
| 334 | 336 | writeFull := func(clear bool) { | |
| 335 | 337 | if clear { | |
| 336 | 338 | w.WriteString(SeqDeleteKittyImages) | |
| 337 | - | w.WriteString(SeqClearScreen) | |
| 339 | + | w.WriteString(SeqClearScreenNoHome) | |
| 338 | 340 | w.WriteString(SeqClearScrollback) | |
| 339 | 341 | w.WriteString(MoveTo(1, 1)) | |
| 340 | 342 | } | |
| @@ -79,7 +79,9 @@ func (r *Renderer) Resize(cols, rows int) { | |||||
| 79 | // Clear both screen and scrollback so stale content from | 79 | // Clear both screen and scrollback so stale content from | ||
| 80 | // the old width doesn't bleed through. Move to (1,1) so | 80 | // the old width doesn't bleed through. Move to (1,1) so | ||
| 81 | // the next DrawLog/writeFull starts from a clean slate. | 81 | // the next DrawLog/writeFull starts from a clean slate. | ||
| 82 | - | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreen+SeqClearScrollback+MoveTo(1, 1)) | 82 | + | // Use the no-home variant: the explicit MoveTo below sets |
| 83 | + | // the cursor without triggering VS Code's viewport-snap. | |||
| 84 | + | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreenNoHome+SeqClearScrollback+MoveTo(1, 1)) | |||
| 83 | } | 85 | } | ||
| 84 | } | 86 | } | ||
| 85 | } | 87 | } | ||
| @@ -97,7 +99,7 @@ func (r *Renderer) Clear() { | |||||
| 97 | r.logViewportTop = 0 | 99 | r.logViewportTop = 0 | ||
| 98 | r.logHardwareRow = 0 | 100 | r.logHardwareRow = 0 | ||
| 99 | r.logInit = false | 101 | r.logInit = false | ||
| 100 | - | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreen+SeqClearScrollback+MoveTo(1, 1)) | 102 | + | _, _ = io.WriteString(r.out, SeqDeleteKittyImages+SeqClearScreenNoHome+SeqClearScrollback+MoveTo(1, 1)) |
| 101 | } | 103 | } | ||
| 102 | 104 | ||||
| 103 | // Invalidate forces a full repaint on the next Draw without clearing the | 105 | // Invalidate forces a full repaint on the next Draw without clearing the | ||
| @@ -334,7 +336,7 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int | |||||
| 334 | writeFull := func(clear bool) { | 336 | writeFull := func(clear bool) { | ||
| 335 | if clear { | 337 | if clear { | ||
| 336 | w.WriteString(SeqDeleteKittyImages) | 338 | w.WriteString(SeqDeleteKittyImages) | ||
| 337 | - | w.WriteString(SeqClearScreen) | 339 | + | w.WriteString(SeqClearScreenNoHome) |
| 338 | w.WriteString(SeqClearScrollback) | 340 | w.WriteString(SeqClearScrollback) | ||
| 339 | w.WriteString(MoveTo(1, 1)) | 341 | w.WriteString(MoveTo(1, 1)) | ||
| 340 | } | 342 | } | ||
M internal/tui/terminal.go +8 -0
| @@ -86,6 +86,14 @@ const ( | |||
| 86 | 86 | SeqHideCursor = "\x1b[?25l" | |
| 87 | 87 | SeqShowCursor = "\x1b[?25h" | |
| 88 | 88 | SeqClearScreen = "\x1b[2J\x1b[H" | |
| 89 | + | // SeqClearScreenNoHome is SeqClearScreen without the trailing | |
| 90 | + | // cursor-home (\x1b[H). Use it whenever a MoveTo(...) follows | |
| 91 | + | // immediately. VS Code's integrated terminal interprets a bare | |
| 92 | + | // CUP-no-args during a clear as "snap the viewport scrollbar to | |
| 93 | + | // row 0", which makes the user's scroll position jump on every | |
| 94 | + | // repaint. The explicit MoveTo we emit afterwards positions the | |
| 95 | + | // cursor identically without triggering that snap. | |
| 96 | + | SeqClearScreenNoHome = "\x1b[2J" | |
| 89 | 97 | SeqClearScrollback = "\x1b[3J" | |
| 90 | 98 | SeqClearLine = "\x1b[2K" | |
| 91 | 99 | SeqResetScrollRegion = "\x1b[r" | |
| @@ -86,6 +86,14 @@ const ( | |||||
| 86 | SeqHideCursor = "\x1b[?25l" | 86 | SeqHideCursor = "\x1b[?25l" | ||
| 87 | SeqShowCursor = "\x1b[?25h" | 87 | SeqShowCursor = "\x1b[?25h" | ||
| 88 | SeqClearScreen = "\x1b[2J\x1b[H" | 88 | SeqClearScreen = "\x1b[2J\x1b[H" | ||
| 89 | + | // SeqClearScreenNoHome is SeqClearScreen without the trailing | |||
| 90 | + | // cursor-home (\x1b[H). Use it whenever a MoveTo(...) follows | |||
| 91 | + | // immediately. VS Code's integrated terminal interprets a bare | |||
| 92 | + | // CUP-no-args during a clear as "snap the viewport scrollbar to | |||
| 93 | + | // row 0", which makes the user's scroll position jump on every | |||
| 94 | + | // repaint. The explicit MoveTo we emit afterwards positions the | |||
| 95 | + | // cursor identically without triggering that snap. | |||
| 96 | + | SeqClearScreenNoHome = "\x1b[2J" | |||
| 89 | SeqClearScrollback = "\x1b[3J" | 97 | SeqClearScrollback = "\x1b[3J" | ||
| 90 | SeqClearLine = "\x1b[2K" | 98 | SeqClearLine = "\x1b[2K" | ||
| 91 | SeqResetScrollRegion = "\x1b[r" | 99 | SeqResetScrollRegion = "\x1b[r" | ||