Add spacing around dialog frames
- commit
- 3d7f80eebbe72adafce52bc6d14899a6670dec86
- author
- patriceckhart · · 2026-05-22 16:53 UTC
- parent
- 8cd8410
- signature
- unsigned
- refs
- tag: v0.1.90
3 files · +42 -2
M internal/agent/modes/dialog_frame.go +37 -0
| @@ -33,6 +33,43 @@ func frameRule(th tui.Theme, width int) string { | |||
| 33 | 33 | return th.FG256(th.Muted, strings.Repeat("─", width)) | |
| 34 | 34 | } | |
| 35 | 35 | ||
| 36 | + | // padDialogFrame inserts breathing room between the shared dialog frame | |
| 37 | + | // chrome and its body while keeping frameHeader/frameRule as single-row | |
| 38 | + | // primitives for callers that need exact row accounting. | |
| 39 | + | func padDialogFrame(lines []string) []string { | |
| 40 | + | if len(lines) == 0 { | |
| 41 | + | return lines | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | out := append([]string(nil), lines...) | |
| 45 | + | if isFrameHeaderLine(out[0]) && (len(out) == 1 || strings.TrimSpace(stripANSIBytes(out[1])) != "") { | |
| 46 | + | out = append(out[:1], append([]string{""}, out[1:]...)...) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | last := len(out) - 1 | |
| 50 | + | if last > 0 && isFrameRuleLine(out[last]) && strings.TrimSpace(stripANSIBytes(out[last-1])) != "" { | |
| 51 | + | out = append(out[:last], append([]string{""}, out[last:]...)...) | |
| 52 | + | } | |
| 53 | + | return out | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func isFrameHeaderLine(line string) bool { | |
| 57 | + | return strings.HasPrefix(stripANSIBytes(line), "── ") | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func isFrameRuleLine(line string) bool { | |
| 61 | + | plain := stripANSIBytes(line) | |
| 62 | + | if plain == "" { | |
| 63 | + | return false | |
| 64 | + | } | |
| 65 | + | for _, r := range plain { | |
| 66 | + | if r != '─' { | |
| 67 | + | return false | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | return true | |
| 71 | + | } | |
| 72 | + | ||
| 36 | 73 | // frameHeaderColor is like frameHeader but renders in a caller-supplied | |
| 37 | 74 | // 256-color code. Used by the update-available banner which wants a | |
| 38 | 75 | // yellow accent on the rules and title. | |
| @@ -33,6 +33,43 @@ func frameRule(th tui.Theme, width int) string { | |||||
| 33 | return th.FG256(th.Muted, strings.Repeat("─", width)) | 33 | return th.FG256(th.Muted, strings.Repeat("─", width)) | ||
| 34 | } | 34 | } | ||
| 35 | 35 | ||||
| 36 | + | // padDialogFrame inserts breathing room between the shared dialog frame | |||
| 37 | + | // chrome and its body while keeping frameHeader/frameRule as single-row | |||
| 38 | + | // primitives for callers that need exact row accounting. | |||
| 39 | + | func padDialogFrame(lines []string) []string { | |||
| 40 | + | if len(lines) == 0 { | |||
| 41 | + | return lines | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | out := append([]string(nil), lines...) | |||
| 45 | + | if isFrameHeaderLine(out[0]) && (len(out) == 1 || strings.TrimSpace(stripANSIBytes(out[1])) != "") { | |||
| 46 | + | out = append(out[:1], append([]string{""}, out[1:]...)...) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | last := len(out) - 1 | |||
| 50 | + | if last > 0 && isFrameRuleLine(out[last]) && strings.TrimSpace(stripANSIBytes(out[last-1])) != "" { | |||
| 51 | + | out = append(out[:last], append([]string{""}, out[last:]...)...) | |||
| 52 | + | } | |||
| 53 | + | return out | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | func isFrameHeaderLine(line string) bool { | |||
| 57 | + | return strings.HasPrefix(stripANSIBytes(line), "── ") | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func isFrameRuleLine(line string) bool { | |||
| 61 | + | plain := stripANSIBytes(line) | |||
| 62 | + | if plain == "" { | |||
| 63 | + | return false | |||
| 64 | + | } | |||
| 65 | + | for _, r := range plain { | |||
| 66 | + | if r != '─' { | |||
| 67 | + | return false | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | return true | |||
| 71 | + | } | |||
| 72 | + | ||||
| 36 | // frameHeaderColor is like frameHeader but renders in a caller-supplied | 73 | // frameHeaderColor is like frameHeader but renders in a caller-supplied | ||
| 37 | // 256-color code. Used by the update-available banner which wants a | 74 | // 256-color code. Used by the update-available banner which wants a | ||
| 38 | // yellow accent on the rules and title. | 75 | // yellow accent on the rules and title. | ||
M internal/agent/modes/help.go +2 -2
| @@ -69,7 +69,7 @@ func renderHelpBlock(th tui.Theme, width int) []string { | |||
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | 71 | var out []string | |
| 72 | - | out = append(out, frameHeader(th, "zot help", width)) | |
| 72 | + | out = append(out, frameHeader(th, "zot help", width), "") | |
| 73 | 73 | ||
| 74 | 74 | // commands section | |
| 75 | 75 | out = append(out, tui.Bold("slash commands:")) | |
| @@ -87,6 +87,6 @@ func renderHelpBlock(th tui.Theme, width int) []string { | |||
| 87 | 87 | th.FG256(th.Muted, k[1]))) | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | - | out = append(out, frameRule(th, width), "") | |
| 90 | + | out = append(out, "", frameRule(th, width), "") | |
| 91 | 91 | return out | |
| 92 | 92 | } | |
| @@ -69,7 +69,7 @@ func renderHelpBlock(th tui.Theme, width int) []string { | |||||
| 69 | } | 69 | } | ||
| 70 | 70 | ||||
| 71 | var out []string | 71 | var out []string | ||
| 72 | - | out = append(out, frameHeader(th, "zot help", width)) | 72 | + | out = append(out, frameHeader(th, "zot help", width), "") |
| 73 | 73 | ||||
| 74 | // commands section | 74 | // commands section | ||
| 75 | out = append(out, tui.Bold("slash commands:")) | 75 | out = append(out, tui.Bold("slash commands:")) | ||
| @@ -87,6 +87,6 @@ func renderHelpBlock(th tui.Theme, width int) []string { | |||||
| 87 | th.FG256(th.Muted, k[1]))) | 87 | th.FG256(th.Muted, k[1]))) | ||
| 88 | } | 88 | } | ||
| 89 | 89 | ||||
| 90 | - | out = append(out, frameRule(th, width), "") | 90 | + | out = append(out, "", frameRule(th, width), "") |
| 91 | return out | 91 | return out | ||
| 92 | } | 92 | } | ||
M internal/agent/modes/interactive.go +3 -0
| @@ -931,6 +931,9 @@ func (i *Interactive) redraw() { | |||
| 931 | 931 | case i.extPanel.Active(): | |
| 932 | 932 | dialog = i.extPanel.Render(i.cfg.Theme, cols) | |
| 933 | 933 | } | |
| 934 | + | if len(dialog) > 0 { | |
| 935 | + | dialog = padDialogFrame(dialog) | |
| 936 | + | } | |
| 934 | 937 | ||
| 935 | 938 | // Slash-command autocomplete: popup above the status line, only | |
| 936 | 939 | // when the editor starts with "/" and no dialog is already open. | |
| @@ -931,6 +931,9 @@ func (i *Interactive) redraw() { | |||||
| 931 | case i.extPanel.Active(): | 931 | case i.extPanel.Active(): | ||
| 932 | dialog = i.extPanel.Render(i.cfg.Theme, cols) | 932 | dialog = i.extPanel.Render(i.cfg.Theme, cols) | ||
| 933 | } | 933 | } | ||
| 934 | + | if len(dialog) > 0 { | |||
| 935 | + | dialog = padDialogFrame(dialog) | |||
| 936 | + | } | |||
| 934 | 937 | ||||
| 935 | // Slash-command autocomplete: popup above the status line, only | 938 | // Slash-command autocomplete: popup above the status line, only | ||
| 936 | // when the editor starts with "/" and no dialog is already open. | 939 | // when the editor starts with "/" and no dialog is already open. | ||