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() { | |||
| 822 | 822 | start = alignSliceStartToImageBlock(chat, start, end) | |
| 823 | 823 | visibleChat = chat[start:end] | |
| 824 | 824 | } | |
| 825 | + | visibleChat = clipBottomClippedImages(visibleChat) | |
| 825 | 826 | ||
| 826 | 827 | // A tiny "scrolled up" indicator in the top-right of the chat pane | |
| 827 | 828 | // 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 | 896 | 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 | 930 | // truncateLine shortens s so it fits within n display cells, with an | |
| 899 | 931 | // ellipsis if trimmed. Used by the "sliding in" chips so a pasted | |
| 900 | 932 | // 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 pane | 827 | // 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 was | 828 | // 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 start | 896 | 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 an | 930 | // truncateLine shortens s so it fits within n display cells, with an | ||
| 899 | // ellipsis if trimmed. Used by the "sliding in" chips so a pasted | 931 | // 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. | ||