detached 33a865b

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 {
415415 // enter the alternate-screen buffer (CSI ?1049h). The renderer emits
416416 // chat as normal terminal flow/scrollback and redraws only the live
417417 // 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)))
419419 defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqBracketedPasteOff + tui.SeqShowCursor))
420420
421421 // 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 emits415 // enter the alternate-screen buffer (CSI ?1049h). The renderer emits
416 // chat as normal terminal flow/scrollback and redraws only the live416 // 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 rate421 // 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) {
7979 // Clear both screen and scrollback so stale content from
8080 // the old width doesn't bleed through. Move to (1,1) so
8181 // 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))
8385 }
8486 }
8587 }
@@ -97,7 +99,7 @@ func (r *Renderer) Clear() {
9799 r.logViewportTop = 0
98100 r.logHardwareRow = 0
99101 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))
101103 }
102104
103105 // 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
334336 writeFull := func(clear bool) {
335337 if clear {
336338 w.WriteString(SeqDeleteKittyImages)
337- w.WriteString(SeqClearScreen)
339+ w.WriteString(SeqClearScreenNoHome)
338340 w.WriteString(SeqClearScrollback)
339341 w.WriteString(MoveTo(1, 1))
340342 }
@@ -79,7 +79,9 @@ func (r *Renderer) Resize(cols, rows int) {
79 // Clear both screen and scrollback so stale content from79 // Clear both screen and scrollback so stale content from
80 // the old width doesn't bleed through. Move to (1,1) so80 // 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 = 099 r.logViewportTop = 0
98 r.logHardwareRow = 0100 r.logHardwareRow = 0
99 r.logInit = false101 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 the105 // 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 (
8686 SeqHideCursor = "\x1b[?25l"
8787 SeqShowCursor = "\x1b[?25h"
8888 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"
8997 SeqClearScrollback = "\x1b[3J"
9098 SeqClearLine = "\x1b[2K"
9199 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"

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close