fix(modes): constrain subagent dialog height
- commit
- eb05ca1fa5a23916988aec93f051c098f8edab89
- author
- brice · · 2026-08-14 16:30 UTC
- parent
- 79ced32
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +38 -3
M packages/agent/modes/resident_subagents_dialog.go +18 -3
| @@ -105,9 +105,14 @@ func (d *residentSubagentsDialog) Render(theme tui.Theme, width, height int) []s | |||
| 105 | 105 | lines := []string{" Resident subagents", " Enter: open Esc: close /subagents new <task>: spawn"} | |
| 106 | 106 | if len(d.rows) == 0 { | |
| 107 | 107 | if d.total > 0 { | |
| 108 | - | return lines | |
| 108 | + | return limitResidentSubagentsDialogHeight(lines, height) | |
| 109 | 109 | } | |
| 110 | - | return append(lines, "", " No resident subagents.") | |
| 110 | + | if height == len(lines)+1 { | |
| 111 | + | lines = append(lines, " No resident subagents.") | |
| 112 | + | } else { | |
| 113 | + | lines = append(lines, "", " No resident subagents.") | |
| 114 | + | } | |
| 115 | + | return limitResidentSubagentsDialogHeight(lines, height) | |
| 111 | 116 | } | |
| 112 | 117 | for index, row := range d.rows { | |
| 113 | 118 | marker := " " | |
| @@ -133,7 +138,17 @@ func (d *residentSubagentsDialog) Render(theme tui.Theme, width, height int) []s | |||
| 133 | 138 | if d.total > len(d.rows) { | |
| 134 | 139 | lines = append(lines, fmt.Sprintf(" %d-%d of %d", d.offset+1, d.offset+len(d.rows), d.total)) | |
| 135 | 140 | } | |
| 136 | - | return lines | |
| 141 | + | return limitResidentSubagentsDialogHeight(lines, height) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func limitResidentSubagentsDialogHeight(lines []string, height int) []string { | |
| 145 | + | if height <= 0 { | |
| 146 | + | return nil | |
| 147 | + | } | |
| 148 | + | if len(lines) <= height { | |
| 149 | + | return lines | |
| 150 | + | } | |
| 151 | + | return lines[:height] | |
| 137 | 152 | } | |
| 138 | 153 | ||
| 139 | 154 | func residentDisplayName(row subagents.ResidentSnapshot) string { | |
| @@ -105,9 +105,14 @@ func (d *residentSubagentsDialog) Render(theme tui.Theme, width, height int) []s | |||||
| 105 | lines := []string{" Resident subagents", " Enter: open Esc: close /subagents new <task>: spawn"} | 105 | lines := []string{" Resident subagents", " Enter: open Esc: close /subagents new <task>: spawn"} | ||
| 106 | if len(d.rows) == 0 { | 106 | if len(d.rows) == 0 { | ||
| 107 | if d.total > 0 { | 107 | if d.total > 0 { | ||
| 108 | - | return lines | 108 | + | return limitResidentSubagentsDialogHeight(lines, height) |
| 109 | } | 109 | } | ||
| 110 | - | return append(lines, "", " No resident subagents.") | 110 | + | if height == len(lines)+1 { |
| 111 | + | lines = append(lines, " No resident subagents.") | |||
| 112 | + | } else { | |||
| 113 | + | lines = append(lines, "", " No resident subagents.") | |||
| 114 | + | } | |||
| 115 | + | return limitResidentSubagentsDialogHeight(lines, height) | |||
| 111 | } | 116 | } | ||
| 112 | for index, row := range d.rows { | 117 | for index, row := range d.rows { | ||
| 113 | marker := " " | 118 | marker := " " | ||
| @@ -133,7 +138,17 @@ func (d *residentSubagentsDialog) Render(theme tui.Theme, width, height int) []s | |||||
| 133 | if d.total > len(d.rows) { | 138 | if d.total > len(d.rows) { | ||
| 134 | lines = append(lines, fmt.Sprintf(" %d-%d of %d", d.offset+1, d.offset+len(d.rows), d.total)) | 139 | lines = append(lines, fmt.Sprintf(" %d-%d of %d", d.offset+1, d.offset+len(d.rows), d.total)) | ||
| 135 | } | 140 | } | ||
| 136 | - | return lines | 141 | + | return limitResidentSubagentsDialogHeight(lines, height) |
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | func limitResidentSubagentsDialogHeight(lines []string, height int) []string { | |||
| 145 | + | if height <= 0 { | |||
| 146 | + | return nil | |||
| 147 | + | } | |||
| 148 | + | if len(lines) <= height { | |||
| 149 | + | return lines | |||
| 150 | + | } | |||
| 151 | + | return lines[:height] | |||
| 137 | } | 152 | } | ||
| 138 | 153 | ||||
| 139 | func residentDisplayName(row subagents.ResidentSnapshot) string { | 154 | func residentDisplayName(row subagents.ResidentSnapshot) string { | ||
M packages/agent/modes/resident_subagents_dialog_test.go +20 -0
| @@ -3,6 +3,7 @@ package modes | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "slices" | |
| 6 | 7 | "strings" | |
| 7 | 8 | "testing" | |
| 8 | 9 | "time" | |
| @@ -73,6 +74,25 @@ func TestResidentSubagentsDialogShowsUsageMetadata(t *testing.T) { | |||
| 73 | 74 | } | |
| 74 | 75 | } | |
| 75 | 76 | ||
| 77 | + | func TestResidentSubagentsDialogBoundsEmptyStateByHeight(t *testing.T) { | |
| 78 | + | dialog := newResidentSubagentsDialog() | |
| 79 | + | dialog.Open(nil) | |
| 80 | + | tests := []struct { | |
| 81 | + | height int | |
| 82 | + | want []string | |
| 83 | + | }{ | |
| 84 | + | {height: 0, want: nil}, | |
| 85 | + | {height: 1, want: []string{" Resident subagents"}}, | |
| 86 | + | {height: 2, want: []string{" Resident subagents", " Enter: open Esc: close /subagents new <task>: spawn"}}, | |
| 87 | + | {height: 3, want: []string{" Resident subagents", " Enter: open Esc: close /subagents new <task>: spawn", " No resident subagents."}}, | |
| 88 | + | } | |
| 89 | + | for _, test := range tests { | |
| 90 | + | if got := plainResidentIndicatorLines(dialog.Render(tui.Dark, 100, test.height)); !slices.Equal(got, test.want) { | |
| 91 | + | t.Errorf("height %d rendered %q, want %q", test.height, got, test.want) | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 76 | 96 | func TestResidentResultStatusIncludesFinalSummary(t *testing.T) { | |
| 77 | 97 | got := residentResultStatus("child", subagents.ResidentResult{State: subagents.ResidentIdle, Summary: "the child answer"}) | |
| 78 | 98 | if got != "completed result child: the child answer" { | |
| @@ -3,6 +3,7 @@ package modes | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "slices" | |||
| 6 | "strings" | 7 | "strings" | ||
| 7 | "testing" | 8 | "testing" | ||
| 8 | "time" | 9 | "time" | ||
| @@ -73,6 +74,25 @@ func TestResidentSubagentsDialogShowsUsageMetadata(t *testing.T) { | |||||
| 73 | } | 74 | } | ||
| 74 | } | 75 | } | ||
| 75 | 76 | ||||
| 77 | + | func TestResidentSubagentsDialogBoundsEmptyStateByHeight(t *testing.T) { | |||
| 78 | + | dialog := newResidentSubagentsDialog() | |||
| 79 | + | dialog.Open(nil) | |||
| 80 | + | tests := []struct { | |||
| 81 | + | height int | |||
| 82 | + | want []string | |||
| 83 | + | }{ | |||
| 84 | + | {height: 0, want: nil}, | |||
| 85 | + | {height: 1, want: []string{" Resident subagents"}}, | |||
| 86 | + | {height: 2, want: []string{" Resident subagents", " Enter: open Esc: close /subagents new <task>: spawn"}}, | |||
| 87 | + | {height: 3, want: []string{" Resident subagents", " Enter: open Esc: close /subagents new <task>: spawn", " No resident subagents."}}, | |||
| 88 | + | } | |||
| 89 | + | for _, test := range tests { | |||
| 90 | + | if got := plainResidentIndicatorLines(dialog.Render(tui.Dark, 100, test.height)); !slices.Equal(got, test.want) { | |||
| 91 | + | t.Errorf("height %d rendered %q, want %q", test.height, got, test.want) | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 76 | func TestResidentResultStatusIncludesFinalSummary(t *testing.T) { | 96 | func TestResidentResultStatusIncludesFinalSummary(t *testing.T) { | ||
| 77 | got := residentResultStatus("child", subagents.ResidentResult{State: subagents.ResidentIdle, Summary: "the child answer"}) | 97 | got := residentResultStatus("child", subagents.ResidentResult{State: subagents.ResidentIdle, Summary: "the child answer"}) | ||
| 78 | if got != "completed result child: the child answer" { | 98 | if got != "completed result child: the child answer" { | ||