Keep settings option pickers within the row cap
- commit
- f425a49339173c4ae18dc7ef4f8fbf632e64ab05
- author
- patriceckhart · · 2026-09-26 10:39 UTC
- parent
- 072bd14
- signature
- unsigned
2 files · +34 -1
M packages/agent/modes/settings_dialog.go +3 -1
| @@ -446,7 +446,9 @@ func (d *settingsDialog) renderOptions(th tui.Theme, width int) []string { | |||
| 446 | 446 | title = d.title | |
| 447 | 447 | } | |
| 448 | 448 | lines := []string{frameHeader(th, title, width)} | |
| 449 | - | if it.desc != "" { | |
| 449 | + | // Keep room for the hint, closing rule, and at least the selected | |
| 450 | + | // option's label before spending a row on the optional description. | |
| 451 | + | if it.desc != "" && (d.MaxRows <= 0 || d.MaxRows >= 5) { | |
| 450 | 452 | lines = append(lines, th.FG256(th.Muted, it.desc)) | |
| 451 | 453 | } | |
| 452 | 454 | lines = append(lines, th.FG256(th.Muted, "select with enter/space, esc to go back:")) | |
| @@ -446,7 +446,9 @@ func (d *settingsDialog) renderOptions(th tui.Theme, width int) []string { | |||||
| 446 | title = d.title | 446 | title = d.title | ||
| 447 | } | 447 | } | ||
| 448 | lines := []string{frameHeader(th, title, width)} | 448 | lines := []string{frameHeader(th, title, width)} | ||
| 449 | - | if it.desc != "" { | 449 | + | // Keep room for the hint, closing rule, and at least the selected |
| 450 | + | // option's label before spending a row on the optional description. | |||
| 451 | + | if it.desc != "" && (d.MaxRows <= 0 || d.MaxRows >= 5) { | |||
| 450 | lines = append(lines, th.FG256(th.Muted, it.desc)) | 452 | lines = append(lines, th.FG256(th.Muted, it.desc)) | ||
| 451 | } | 453 | } | ||
| 452 | lines = append(lines, th.FG256(th.Muted, "select with enter/space, esc to go back:")) | 454 | lines = append(lines, th.FG256(th.Muted, "select with enter/space, esc to go back:")) | ||
M packages/agent/modes/settings_dialog_test.go +31 -0
| @@ -127,6 +127,37 @@ func TestSettingsDialogKeepsCursorRowOnTinyTerminal(t *testing.T) { | |||
| 127 | 127 | } | |
| 128 | 128 | } | |
| 129 | 129 | ||
| 130 | + | func TestSettingsOptionPickerFitsShortTerminal(t *testing.T) { | |
| 131 | + | for _, rows := range []int{12, 14, 16, 17, 24} { | |
| 132 | + | t.Run(fmt.Sprintf("rows=%d", rows), func(t *testing.T) { | |
| 133 | + | term := &shortTestTerminal{cols: 80, rows: rows} | |
| 134 | + | i := NewInteractive(InteractiveConfig{Terminal: term}) | |
| 135 | + | i.settingsDialog.OpenDirectOption(settingsItem{ | |
| 136 | + | label: "reasoning level", desc: "reasoning depth for reasoning-capable models", | |
| 137 | + | options: []settingsOption{{label: "off"}, {label: "low"}}, | |
| 138 | + | }) | |
| 139 | + | i.redraw() | |
| 140 | + | ||
| 141 | + | lines := i.settingsDialog.Render(i.cfg.Theme, 80) | |
| 142 | + | if len(lines) > i.settingsDialog.MaxRows { | |
| 143 | + | t.Fatalf("option picker rendered %d rows, cap %d", len(lines), i.settingsDialog.MaxRows) | |
| 144 | + | } | |
| 145 | + | text := renderedPlain(lines) | |
| 146 | + | if !strings.Contains(text, "off") { | |
| 147 | + | t.Fatalf("selected option missing from window:\n%s", text) | |
| 148 | + | } | |
| 149 | + | if i.settingsDialog.MaxRows >= 5 && !strings.Contains(text, "reasoning depth") { | |
| 150 | + | t.Fatalf("description missing when it fits:\n%s", text) | |
| 151 | + | } | |
| 152 | + | written := strings.Count(term.String(), "\r\n") + 1 | |
| 153 | + | band := written - len(i.cachedChatLocked(80)) | |
| 154 | + | if band > rows-1 { | |
| 155 | + | t.Fatalf("bottom band is %d rows for a %d-row terminal", band, rows) | |
| 156 | + | } | |
| 157 | + | }) | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | ||
| 130 | 161 | func TestSettingsDialogRendersEverythingWithoutMaxRows(t *testing.T) { | |
| 131 | 162 | d := newSettingsDialog() | |
| 132 | 163 | d.Open(settingsTestItems(20)) | |
| @@ -127,6 +127,37 @@ func TestSettingsDialogKeepsCursorRowOnTinyTerminal(t *testing.T) { | |||||
| 127 | } | 127 | } | ||
| 128 | } | 128 | } | ||
| 129 | 129 | ||||
| 130 | + | func TestSettingsOptionPickerFitsShortTerminal(t *testing.T) { | |||
| 131 | + | for _, rows := range []int{12, 14, 16, 17, 24} { | |||
| 132 | + | t.Run(fmt.Sprintf("rows=%d", rows), func(t *testing.T) { | |||
| 133 | + | term := &shortTestTerminal{cols: 80, rows: rows} | |||
| 134 | + | i := NewInteractive(InteractiveConfig{Terminal: term}) | |||
| 135 | + | i.settingsDialog.OpenDirectOption(settingsItem{ | |||
| 136 | + | label: "reasoning level", desc: "reasoning depth for reasoning-capable models", | |||
| 137 | + | options: []settingsOption{{label: "off"}, {label: "low"}}, | |||
| 138 | + | }) | |||
| 139 | + | i.redraw() | |||
| 140 | + | ||||
| 141 | + | lines := i.settingsDialog.Render(i.cfg.Theme, 80) | |||
| 142 | + | if len(lines) > i.settingsDialog.MaxRows { | |||
| 143 | + | t.Fatalf("option picker rendered %d rows, cap %d", len(lines), i.settingsDialog.MaxRows) | |||
| 144 | + | } | |||
| 145 | + | text := renderedPlain(lines) | |||
| 146 | + | if !strings.Contains(text, "off") { | |||
| 147 | + | t.Fatalf("selected option missing from window:\n%s", text) | |||
| 148 | + | } | |||
| 149 | + | if i.settingsDialog.MaxRows >= 5 && !strings.Contains(text, "reasoning depth") { | |||
| 150 | + | t.Fatalf("description missing when it fits:\n%s", text) | |||
| 151 | + | } | |||
| 152 | + | written := strings.Count(term.String(), "\r\n") + 1 | |||
| 153 | + | band := written - len(i.cachedChatLocked(80)) | |||
| 154 | + | if band > rows-1 { | |||
| 155 | + | t.Fatalf("bottom band is %d rows for a %d-row terminal", band, rows) | |||
| 156 | + | } | |||
| 157 | + | }) | |||
| 158 | + | } | |||
| 159 | + | } | |||
| 160 | + | ||||
| 130 | func TestSettingsDialogRendersEverythingWithoutMaxRows(t *testing.T) { | 161 | func TestSettingsDialogRendersEverythingWithoutMaxRows(t *testing.T) { | ||
| 131 | d := newSettingsDialog() | 162 | d := newSettingsDialog() | ||
| 132 | d.Open(settingsTestItems(20)) | 163 | d.Open(settingsTestItems(20)) | ||