fix(tui): keep sidebar panels inside the terminal height

This commit is contained in:
Ahmed Allam 2026-10-04 01:05:43 +00:00 • committed by Ahmed Allam
parent 701c22b8a9
commit 1a680f4aa3
3 changed files with 64 additions and 2 deletions

View file

@ -220,6 +220,8 @@ func (m *Model) revealPanel(panel sidebarPanel) {
func (m *Model) clickPanel(rect panelRect, x, y int) bool {
switch {
case rect.height <= 1 && !m.panelShrunk(rect.panel):
m.togglePanelZoom(rect.panel)
case rect.height <= 1:
m.revealPanel(rect.panel)
case y-rect.top != 1:

View file

@ -230,3 +230,45 @@ func TestScrollbarHitTestFollowsPanelState(t *testing.T) {
t.Fatalf("collapsed panel still reports a scrollbar")
}
}
func TestSidebarFitsShortTerminal(t *testing.T) {
m := panelsModel(t)
m.height = 19
m.snapshot.Connections = m.snapshot.Connections[:1]
m.resizeViewport()
_, sidebarWidth, _, _ := m.layout()
rects := m.sidebarPanels()
last := rects[len(rects)-1]
if last.top+last.height > m.height {
t.Fatalf("panels run past the screen: %+v (height %d)", rects, m.height)
}
if got := lipgloss.Height(m.sidebarView(sidebarWidth, m.height)); got > m.height {
t.Fatalf("sidebar renders %d rows on a %d-row terminal", got, m.height)
}
squeezed, ok := m.panelAt(last.top)
if !ok || squeezed.panel != panelStats || squeezed.height != 1 {
t.Fatalf("expected the stats panel squeezed to its header, got %+v", squeezed)
}
m = click(t, m, m.width-6, last.top)
if m.zoomedPanel != panelStats {
t.Fatalf("clicking a squeezed header should zoom it, zoomed=%v", m.zoomedPanel)
}
statsHeight, _, _, _ := m.sidebarHeights()
if statsHeight < 4 {
t.Fatalf("zoomed stats panel still has no room: %d", statsHeight)
}
}
func TestStatsPanelKeepsAssignedHeight(t *testing.T) {
m := panelsModel(t)
m.snapshot.Model = strings.Repeat("openrouter/some-vendor/a-very-long-model-name ", 12)
m.resizeViewport()
_, sidebarWidth, _, _ := m.layout()
statsHeight, _, _, _ := m.sidebarHeights()
if statsHeight != 15 {
t.Fatalf("stats panel should hit its cap, got %d", statsHeight)
}
if got := lipgloss.Height(m.sidebarView(sidebarWidth, m.height)); got != m.height {
t.Fatalf("sidebar renders %d rows, want %d", got, m.height)
}
}

View file

@ -582,7 +582,7 @@ func (m Model) sidebarView(width, height int) string {
}
stats := ""
if statsHeight > 1 {
stats = m.statsView()
stats = fixedPanelBody(lipgloss.NewStyle().Width(width-4).Render(m.statsView()), width-4, statsHeight-3)
}
parts = append(parts, m.panelBox(panelStats, stats, width, statsHeight, false))
return lipgloss.JoinVertical(lipgloss.Left, parts...)
@ -603,7 +603,25 @@ func (m Model) sidebarHeights() (statsHeight, vulnHeight, mcpHeight, agentHeight
if len(m.snapshot.Connections) > 0 {
mcpHeight = m.panelHeight(panelMcp, min(9, len(m.snapshot.Connections)+3))
}
agentHeight = m.panelHeight(panelAgents, 3)
agentHeight = m.panelHeight(panelAgents, 4)
squeezable := []*int{&statsHeight, &mcpHeight, &vulnHeight}
for _, h := range squeezable {
over := m.viewerHeight() + statsHeight + vulnHeight + mcpHeight + agentHeight - m.height
if over <= 0 {
break
}
if *h > 4 {
*h -= min(over, *h-4)
}
}
for _, h := range squeezable {
if m.viewerHeight()+statsHeight+vulnHeight+mcpHeight+agentHeight <= m.height {
break
}
if *h > 1 {
*h = 1
}
}
spare := max(0, m.height-m.viewerHeight()-statsHeight-vulnHeight-mcpHeight-agentHeight)
switch {
case m.zoomedPanel == panelFindings && vulnHeight > 1: