fix(tui): wrap long artifact lines in viewer instead of truncating

Artifact content wider than the modal was silently cut off. Lines are now
hard-wrapped at rune boundaries to the modal content width, and scroll
clamping uses the wrapped line count so the last page stays reachable.
This commit is contained in:
2026-06-10 20:54:39 +04:00
parent cba9dd4583
commit c82a86477d
2 changed files with 49 additions and 10 deletions
+47 -9
View File
@@ -440,8 +440,9 @@ func (m Model) artifactContentBodyH() int {
return h
}
// artifactContentLines splits the selected artifact's content into display lines.
func (m Model) artifactContentLines() []string {
// artifactContentLines splits the selected artifact's content into display lines,
// wrapping each line to the specified width.
func (m Model) artifactContentLines(w int) []string {
if m.artifactsIndex < 0 || m.artifactsIndex >= len(m.artifacts) {
return nil
}
@@ -449,12 +450,13 @@ func (m Model) artifactContentLines() []string {
if c == nil {
return []string{"(no content stored)"}
}
return strings.Split(strings.ReplaceAll(*c, "\r\n", "\n"), "\n")
lines := strings.Split(strings.ReplaceAll(*c, "\r\n", "\n"), "\n")
return wrapLines(lines, w)
}
// artifactContentMaxScroll keeps the last page of content anchored to the body bottom.
func (m Model) artifactContentMaxScroll() int {
max := len(m.artifactContentLines()) - m.artifactContentBodyH()
func (m Model) artifactContentMaxScroll(w int) int {
max := len(m.artifactContentLines(w)) - m.artifactContentBodyH()
if max < 0 {
max = 0
}
@@ -515,11 +517,13 @@ func (m Model) artifactsModal() string {
}
// Selected artifact content, scrollable with PgUp/PgDn.
lines := m.artifactContentLines()
// Width available for content: w - 2 (left padding) - 2 (box borders) - 2 (right padding) = w - 6
contentW := w - 6
lines := m.artifactContentLines(contentW)
bodyH := m.artifactContentBodyH()
coff := m.artifactScroll
if coff > m.artifactContentMaxScroll() {
coff = m.artifactContentMaxScroll()
if coff > m.artifactContentMaxScroll(contentW) {
coff = m.artifactContentMaxScroll(contentW)
}
if coff < 0 {
coff = 0
@@ -534,7 +538,7 @@ func (m Model) artifactsModal() string {
cend = len(lines)
}
for i := coff; i < cend; i++ {
b.WriteString(mbg(t, " "+truncate(lines[i], w-6), t.P.Fg) + "\n")
b.WriteString(mbg(t, " "+lines[i], t.P.Fg) + "\n")
}
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"PgUp/Dn", "scroll"}, {"v/esc", "close"}}))
@@ -582,3 +586,37 @@ func truncate(s string, w int) string {
}
return string(r[:w-1]) + "…"
}
// wrapLines wraps each line in the input slice to the specified width using hard rune-boundary wrapping.
func wrapLines(lines []string, w int) []string {
if w < 1 {
w = 1
}
var result []string
for _, line := range lines {
wrapped := wrapLine(line, w)
result = append(result, wrapped...)
}
return result
}
// wrapLine wraps a single line to the specified width using hard rune-boundary wrapping.
func wrapLine(s string, w int) []string {
if w < 1 {
w = 1
}
runes := []rune(s)
if len(runes) <= w {
return []string{s}
}
var lines []string
for len(runes) > 0 {
if len(runes) <= w {
lines = append(lines, string(runes))
break
}
lines = append(lines, string(runes[:w]))
runes = runes[w:]
}
return lines
}