detached dad49e9

tweak(tui): image-block spacing + indent

Two small fixes to how inline-image tool results render.

1) Extra blank row under the image.

   RowsForInlineImage estimates the on-screen height from a
   fixed cell aspect ratio. Real terminals (iTerm2, Ghostty,
   Kitty, WezTerm) don't all match that ratio exactly, so the
   image often paints one row past the estimate and the info
   line below it ("image - image/png - 612x904 - 99 KB") gets
   overwritten by the image's bottom edge. Reserving one extra
   blank row between the image and the info line absorbs that
   spillover on every terminal I tried.

   Same symmetry for the trailing blank: the info line no
   longer sits flush against the tool-block closing rule or
   the next content below.

2) Info line indented to 4 spaces.

   Every other tool-output row (renderRawFile's gutter,
   renderBashResult, diff rows, the "... N more lines, M total"
   collapse footer) starts at column 4. The image info line
   used to start at column 2, which looked off next to the
   other rows. Bumped it to 4 so the metadata line aligns with
   the file content above and below it.

Text-only fallback (terminals without inline image support)
also picks up the trailing blank + 4-space indent so the two\nrender paths behave consistently.
commit
dad49e9b7a1d7046be7f6707ce606c456b869d80
author
patriceckhart · · 2026-04-21 17:12 UTC
parent
112341c
signature
unsigned
refs
tag: v0.0.84
1 file · +21 -6
M internal/tui/view.go +21 -6
@@ -841,7 +841,11 @@ func (v *View) renderDiffRow(line string, width, color int, lineNo int, mark byt
841841 func (v *View) renderImageBlock(b provider.ImageBlock, width int) []string {
842842 w, h := ImageDimensions(b.Data)
843843 kb := len(b.Data) / 1024
844- info := fmt.Sprintf(" image - %s - %dx%d - %d KB", b.MimeType, w, h, kb)
844+ // Four-space indent matches every other tool-output row
845+ // (renderRawFile, renderBashResult, diff rows, "... N more
846+ // lines" footers). Keeps the image's metadata line vertically
847+ // aligned with the file content above / below it.
848+ info := fmt.Sprintf(" image - %s - %dx%d - %d KB", b.MimeType, w, h, kb)
845849
846850 if v.ImageProto != ImageProtocolNone {
847851 // Clamp rendered width so the image never overflows the chat
@@ -860,18 +864,29 @@ func (v *View) renderImageBlock(b provider.ImageBlock, width int) []string {
860864 if rows < 1 {
861865 rows = 1
862866 }
863- out := make([]string, 0, rows+1)
867+ out := make([]string, 0, rows+3)
864868 out = append(out, " "+seq)
865- // Reserve blank rows matching the image's on-screen height.
866- for i := 1; i < rows; i++ {
869+ // Reserve blank rows matching the image's on-screen
870+ // height, plus one extra. RowsForInlineImage estimates
871+ // the height from a fixed cell aspect ratio; real
872+ // terminals differ enough that the image often spills
873+ // one row past the estimate and repaints over the info
874+ // line below. The extra row absorbs that spillover.
875+ for i := 1; i < rows+1; i++ {
867876 out = append(out, "")
868877 }
869878 out = append(out, v.Theme.FG256(v.Theme.Muted, info))
879+ // Trailing blank so the closing rule / next block has
880+ // some breathing room under the info line instead of
881+ // sitting flush against it.
882+ out = append(out, "")
870883 return out
871884 }
872885 }
873- // Text fallback.
874- return []string{v.Theme.FG256(v.Theme.Muted, info)}
886+ // Text fallback: same trailing blank for symmetry with the
887+ // inline path, so a text-only image placeholder also gets
888+ // a little space below before the next content.
889+ return []string{v.Theme.FG256(v.Theme.Muted, info), ""}
875890 }
876891
877892 // looksLikeNumberedFile returns true when text matches the `read`
@@ -841,7 +841,11 @@ func (v *View) renderDiffRow(line string, width, color int, lineNo int, mark byt
841 func (v *View) renderImageBlock(b provider.ImageBlock, width int) []string {841 func (v *View) renderImageBlock(b provider.ImageBlock, width int) []string {
842 w, h := ImageDimensions(b.Data)842 w, h := ImageDimensions(b.Data)
843 kb := len(b.Data) / 1024843 kb := len(b.Data) / 1024
844- info := fmt.Sprintf(" image - %s - %dx%d - %d KB", b.MimeType, w, h, kb)844+ // Four-space indent matches every other tool-output row
845+ // (renderRawFile, renderBashResult, diff rows, "... N more
846+ // lines" footers). Keeps the image's metadata line vertically
847+ // aligned with the file content above / below it.
848+ info := fmt.Sprintf(" image - %s - %dx%d - %d KB", b.MimeType, w, h, kb)
845 849
846 if v.ImageProto != ImageProtocolNone {850 if v.ImageProto != ImageProtocolNone {
847 // Clamp rendered width so the image never overflows the chat851 // Clamp rendered width so the image never overflows the chat
@@ -860,18 +864,29 @@ func (v *View) renderImageBlock(b provider.ImageBlock, width int) []string {
860 if rows < 1 {864 if rows < 1 {
861 rows = 1865 rows = 1
862 }866 }
863- out := make([]string, 0, rows+1)867+ out := make([]string, 0, rows+3)
864 out = append(out, " "+seq)868 out = append(out, " "+seq)
865- // Reserve blank rows matching the image's on-screen height.869+ // Reserve blank rows matching the image's on-screen
866- for i := 1; i < rows; i++ {870+ // height, plus one extra. RowsForInlineImage estimates
871+ // the height from a fixed cell aspect ratio; real
872+ // terminals differ enough that the image often spills
873+ // one row past the estimate and repaints over the info
874+ // line below. The extra row absorbs that spillover.
875+ for i := 1; i < rows+1; i++ {
867 out = append(out, "")876 out = append(out, "")
868 }877 }
869 out = append(out, v.Theme.FG256(v.Theme.Muted, info))878 out = append(out, v.Theme.FG256(v.Theme.Muted, info))
879+ // Trailing blank so the closing rule / next block has
880+ // some breathing room under the info line instead of
881+ // sitting flush against it.
882+ out = append(out, "")
870 return out883 return out
871 }884 }
872 }885 }
873- // Text fallback.886+ // Text fallback: same trailing blank for symmetry with the
874- return []string{v.Theme.FG256(v.Theme.Muted, info)}887+ // inline path, so a text-only image placeholder also gets
888+ // a little space below before the next content.
889+ return []string{v.Theme.FG256(v.Theme.Muted, info), ""}
875 }890 }
876 891
877 // looksLikeNumberedFile returns true when text matches the `read`892 // looksLikeNumberedFile returns true when text matches the `read`

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