From 1a680f4aa3af3b106cd7852f4390f1403b6ebd97 Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Sun, 4 Oct 2026 01:05:43 +0000 Subject: [PATCH] fix(tui): keep sidebar panels inside the terminal height --- strix/interface/tui/internal/app/panels.go | 2 + .../interface/tui/internal/app/panels_test.go | 42 +++++++++++++++++++ strix/interface/tui/internal/app/view.go | 22 +++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/strix/interface/tui/internal/app/panels.go b/strix/interface/tui/internal/app/panels.go index 0d1b1ec7..2517beee 100644 --- a/strix/interface/tui/internal/app/panels.go +++ b/strix/interface/tui/internal/app/panels.go @@ -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: diff --git a/strix/interface/tui/internal/app/panels_test.go b/strix/interface/tui/internal/app/panels_test.go index 141408f4..c4d175d5 100644 --- a/strix/interface/tui/internal/app/panels_test.go +++ b/strix/interface/tui/internal/app/panels_test.go @@ -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) + } +} diff --git a/strix/interface/tui/internal/app/view.go b/strix/interface/tui/internal/app/view.go index 80435c2c..dd7ad091 100644 --- a/strix/interface/tui/internal/app/view.go +++ b/strix/interface/tui/internal/app/view.go @@ -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: