detached 2cf21d7

fix(tui): suppress clipped images near status bar

commit
2cf21d7bda6c2f9ecc590443ea52c09e2c9304a2
author
patriceckhart · · 2026-04-21 19:32 UTC
parent
a739ef9
signature
unsigned
refs
tag: v0.0.88
1 file · +32 -0
M internal/agent/modes/interactive.go +32 -0
@@ -822,6 +822,7 @@ func (i *Interactive) redraw() {
822822 start = alignSliceStartToImageBlock(chat, start, end)
823823 visibleChat = chat[start:end]
824824 }
825+ visibleChat = clipBottomClippedImages(visibleChat)
825826
826827 // A tiny "scrolled up" indicator in the top-right of the chat pane
827828 // so you know you're not at the bottom. When the viewport was
@@ -895,6 +896,37 @@ func alignSliceStartToImageBlock(chat []string, start, end int) int {
895896 return start
896897 }
897898
899+func clipBottomClippedImages(lines []string) []string {
900+ if len(lines) == 0 {
901+ return lines
902+ }
903+ out := append([]string(nil), lines...)
904+ for i, line := range out {
905+ if !strings.Contains(line, "\x1b]1337;File=") && !strings.Contains(line, "\x1b_G") {
906+ continue
907+ }
908+ // Image blocks render as: image escape, zero or more blank
909+ // reservation rows, then the muted "image - ..." info line,
910+ // then one trailing blank. If the info line isn't visible in
911+ // the current chat slice, the image would paint down into the
912+ // fixed status bar area. Suppress that image for this frame.
913+ foundInfo := false
914+ for j := i + 1; j < len(out); j++ {
915+ if strings.Contains(out[j], "image - ") {
916+ foundInfo = true
917+ break
918+ }
919+ if strings.TrimSpace(out[j]) != "" {
920+ break
921+ }
922+ }
923+ if !foundInfo {
924+ out[i] = ""
925+ }
926+ }
927+ return out
928+}
929+
898930 // truncateLine shortens s so it fits within n display cells, with an
899931 // ellipsis if trimmed. Used by the "sliding in" chips so a pasted
900932 // novel doesn't blow past the status line.
@@ -822,6 +822,7 @@ func (i *Interactive) redraw() {
822 start = alignSliceStartToImageBlock(chat, start, end)822 start = alignSliceStartToImageBlock(chat, start, end)
823 visibleChat = chat[start:end]823 visibleChat = chat[start:end]
824 }824 }
825+ visibleChat = clipBottomClippedImages(visibleChat)
825 826
826 // A tiny "scrolled up" indicator in the top-right of the chat pane827 // A tiny "scrolled up" indicator in the top-right of the chat pane
827 // so you know you're not at the bottom. When the viewport was828 // so you know you're not at the bottom. When the viewport was
@@ -895,6 +896,37 @@ func alignSliceStartToImageBlock(chat []string, start, end int) int {
895 return start896 return start
896 }897 }
897 898
899+func clipBottomClippedImages(lines []string) []string {
900+ if len(lines) == 0 {
901+ return lines
902+ }
903+ out := append([]string(nil), lines...)
904+ for i, line := range out {
905+ if !strings.Contains(line, "\x1b]1337;File=") && !strings.Contains(line, "\x1b_G") {
906+ continue
907+ }
908+ // Image blocks render as: image escape, zero or more blank
909+ // reservation rows, then the muted "image - ..." info line,
910+ // then one trailing blank. If the info line isn't visible in
911+ // the current chat slice, the image would paint down into the
912+ // fixed status bar area. Suppress that image for this frame.
913+ foundInfo := false
914+ for j := i + 1; j < len(out); j++ {
915+ if strings.Contains(out[j], "image - ") {
916+ foundInfo = true
917+ break
918+ }
919+ if strings.TrimSpace(out[j]) != "" {
920+ break
921+ }
922+ }
923+ if !foundInfo {
924+ out[i] = ""
925+ }
926+ }
927+ return out
928+}
929+
898 // truncateLine shortens s so it fits within n display cells, with an930 // truncateLine shortens s so it fits within n display cells, with an
899 // ellipsis if trimmed. Used by the "sliding in" chips so a pasted931 // ellipsis if trimmed. Used by the "sliding in" chips so a pasted
900 // novel doesn't blow past the status line.932 // novel doesn't blow past the status line.

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