Word-wrap provider error rows instead of truncating
A long provider error (e.g. a Bedrock 400 with an embedded JSON body) was appended as one line and clipped at the terminal edge. Wrap v.Err to the build width via wrapLine (which hard-breaks unbreakable blobs), keep the marker on the first row, and indent continuation rows so the whole message is readable.
- commit
- 4dffc8529bae299a0c0395ac242dad45dc41c946
- author
- patriceckhart · · 2026-06-04 17:25 UTC
- parent
- d8976c9
- signature
- unsigned
- refs
- tag: v0.2.9
1 file · +29 -2
M packages/tui/view.go +29 -2
| @@ -263,12 +263,39 @@ func (v *View) BuildLive(width int) []string { | |||
| 263 | 263 | // out = append(out, "") | |
| 264 | 264 | } | |
| 265 | 265 | if v.Err != "" { | |
| 266 | - | out = append(out, v.Theme.FG256(v.Theme.Error, "✖ "+v.Err)) | |
| 266 | + | out = append(out, v.renderErr(width)...) | |
| 267 | 267 | // out = append(out, "") | |
| 268 | 268 | } | |
| 269 | 269 | return out | |
| 270 | 270 | } | |
| 271 | 271 | ||
| 272 | + | // renderErr formats v.Err into one or more colored rows, word-wrapped | |
| 273 | + | // to width so long provider error messages (e.g. a Bedrock 400 with an | |
| 274 | + | // embedded JSON body) aren't cut off at the terminal edge. The first | |
| 275 | + | // row carries the "✖ " marker; continuation rows align under the text | |
| 276 | + | // with a two-cell indent. | |
| 277 | + | func (v *View) renderErr(width int) []string { | |
| 278 | + | const marker = "✖ " | |
| 279 | + | const indent = " " | |
| 280 | + | wrapWidth := width - len([]rune(marker)) | |
| 281 | + | if wrapWidth < 8 { | |
| 282 | + | wrapWidth = 8 | |
| 283 | + | } | |
| 284 | + | wrapped := wrapLine(v.Err, wrapWidth, "") | |
| 285 | + | if len(wrapped) == 0 { | |
| 286 | + | wrapped = []string{""} | |
| 287 | + | } | |
| 288 | + | out := make([]string, 0, len(wrapped)) | |
| 289 | + | for idx, line := range wrapped { | |
| 290 | + | prefix := marker | |
| 291 | + | if idx > 0 { | |
| 292 | + | prefix = indent | |
| 293 | + | } | |
| 294 | + | out = append(out, v.Theme.FG256(v.Theme.Error, prefix+line)) | |
| 295 | + | } | |
| 296 | + | return out | |
| 297 | + | } | |
| 298 | + | ||
| 272 | 299 | // BuildWithAnchors is like Build but additionally reports the first | |
| 273 | 300 | // row occupied by each message in v.Messages. Callers that need to | |
| 274 | 301 | // scroll to a specific turn (the /jump dialog) use the anchor slice | |
| @@ -388,7 +415,7 @@ func (v *View) BuildWithAnchors(width int) ([]string, []MessageAnchor) { | |||
| 388 | 415 | out = append(out, "") | |
| 389 | 416 | } | |
| 390 | 417 | if v.Err != "" { | |
| 391 | - | out = append(out, v.Theme.FG256(v.Theme.Error, "✖ "+v.Err)) | |
| 418 | + | out = append(out, v.renderErr(width)...) | |
| 392 | 419 | out = append(out, "") | |
| 393 | 420 | } | |
| 394 | 421 | return out, anchors | |
| @@ -263,12 +263,39 @@ func (v *View) BuildLive(width int) []string { | |||||
| 263 | // out = append(out, "") | 263 | // out = append(out, "") | ||
| 264 | } | 264 | } | ||
| 265 | if v.Err != "" { | 265 | if v.Err != "" { | ||
| 266 | - | out = append(out, v.Theme.FG256(v.Theme.Error, "✖ "+v.Err)) | 266 | + | out = append(out, v.renderErr(width)...) |
| 267 | // out = append(out, "") | 267 | // out = append(out, "") | ||
| 268 | } | 268 | } | ||
| 269 | return out | 269 | return out | ||
| 270 | } | 270 | } | ||
| 271 | 271 | ||||
| 272 | + | // renderErr formats v.Err into one or more colored rows, word-wrapped | |||
| 273 | + | // to width so long provider error messages (e.g. a Bedrock 400 with an | |||
| 274 | + | // embedded JSON body) aren't cut off at the terminal edge. The first | |||
| 275 | + | // row carries the "✖ " marker; continuation rows align under the text | |||
| 276 | + | // with a two-cell indent. | |||
| 277 | + | func (v *View) renderErr(width int) []string { | |||
| 278 | + | const marker = "✖ " | |||
| 279 | + | const indent = " " | |||
| 280 | + | wrapWidth := width - len([]rune(marker)) | |||
| 281 | + | if wrapWidth < 8 { | |||
| 282 | + | wrapWidth = 8 | |||
| 283 | + | } | |||
| 284 | + | wrapped := wrapLine(v.Err, wrapWidth, "") | |||
| 285 | + | if len(wrapped) == 0 { | |||
| 286 | + | wrapped = []string{""} | |||
| 287 | + | } | |||
| 288 | + | out := make([]string, 0, len(wrapped)) | |||
| 289 | + | for idx, line := range wrapped { | |||
| 290 | + | prefix := marker | |||
| 291 | + | if idx > 0 { | |||
| 292 | + | prefix = indent | |||
| 293 | + | } | |||
| 294 | + | out = append(out, v.Theme.FG256(v.Theme.Error, prefix+line)) | |||
| 295 | + | } | |||
| 296 | + | return out | |||
| 297 | + | } | |||
| 298 | + | ||||
| 272 | // BuildWithAnchors is like Build but additionally reports the first | 299 | // BuildWithAnchors is like Build but additionally reports the first | ||
| 273 | // row occupied by each message in v.Messages. Callers that need to | 300 | // row occupied by each message in v.Messages. Callers that need to | ||
| 274 | // scroll to a specific turn (the /jump dialog) use the anchor slice | 301 | // scroll to a specific turn (the /jump dialog) use the anchor slice | ||
| @@ -388,7 +415,7 @@ func (v *View) BuildWithAnchors(width int) ([]string, []MessageAnchor) { | |||||
| 388 | out = append(out, "") | 415 | out = append(out, "") | ||
| 389 | } | 416 | } | ||
| 390 | if v.Err != "" { | 417 | if v.Err != "" { | ||
| 391 | - | out = append(out, v.Theme.FG256(v.Theme.Error, "✖ "+v.Err)) | 418 | + | out = append(out, v.renderErr(width)...) |
| 392 | out = append(out, "") | 419 | out = append(out, "") | ||
| 393 | } | 420 | } | ||
| 394 | return out, anchors | 421 | return out, anchors | ||