mirror of
https://github.com/usestrix/strix.git
synced 2026-10-04 02:33:47 +00:00
fix(tui): blank row under panel headers; no panel controls when Agents is the only panel
This commit is contained in:
parent
849671f288
commit
d218c29ed1
5 changed files with 61 additions and 14 deletions
|
|
@ -221,7 +221,7 @@ func (m *Model) ensureAgentVisible() {
|
|||
|
||||
func (m Model) agentPageSize() int {
|
||||
_, _, _, agentHeight := m.sidebarHeights()
|
||||
return max(1, agentHeight-3)
|
||||
return max(1, agentHeight-4)
|
||||
}
|
||||
|
||||
func (m *Model) keepAgentSelectionInWindow() {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,12 @@ type panelRect struct {
|
|||
height int
|
||||
}
|
||||
|
||||
func (m Model) panelControls() bool {
|
||||
return len(m.snapshot.Vulnerabilities) > 0 || len(m.snapshot.Connections) > 0
|
||||
}
|
||||
|
||||
func (m Model) panelShrunk(panel sidebarPanel) bool {
|
||||
if panel == panelStats {
|
||||
if panel == panelStats || !m.panelControls() {
|
||||
return false
|
||||
}
|
||||
return m.collapsedPanels[panel] || (m.zoomedPanel != panelNone && m.zoomedPanel != panel)
|
||||
|
|
@ -115,6 +119,9 @@ func (m Model) panelTitle(panel sidebarPanel) string {
|
|||
|
||||
func (m Model) panelHeader(panel sidebarPanel, width int) string {
|
||||
style := lipgloss.NewStyle().Foreground(dim)
|
||||
if !m.panelControls() {
|
||||
return truncate(style.Render(m.panelTitle(panel)), max(1, width))
|
||||
}
|
||||
glyph := panelZoomGlyph
|
||||
if m.zoomedPanel == panel {
|
||||
glyph = panelUnzoomGlyph
|
||||
|
|
@ -145,7 +152,7 @@ func (m Model) panelBox(panel sidebarPanel, body string, width, height int, focu
|
|||
if panel != panelStats {
|
||||
content = m.panelHeader(panel, width-4)
|
||||
if body != "" {
|
||||
content += "\n" + body
|
||||
content += "\n\n" + body
|
||||
}
|
||||
}
|
||||
return lipgloss.NewStyle().Width(width-2).Height(height-2).Border(lipgloss.RoundedBorder()).
|
||||
|
|
@ -246,7 +253,7 @@ func (m *Model) revealPanel(panel sidebarPanel) {
|
|||
|
||||
func (m *Model) clickPanel(rect panelRect, x, y int) bool {
|
||||
switch {
|
||||
case rect.panel == panelStats:
|
||||
case rect.panel == panelStats || !m.panelControls():
|
||||
return false
|
||||
case rect.height <= 1 && !m.panelShrunk(rect.panel):
|
||||
m.togglePanelZoom(rect.panel)
|
||||
|
|
|
|||
|
|
@ -318,3 +318,40 @@ func TestStatsPanelKeepsAssignedHeight(t *testing.T) {
|
|||
t.Fatalf("sidebar renders %d rows, want %d", got, m.height)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadersAreFollowedByABlankRow(t *testing.T) {
|
||||
m := panelsModel(t)
|
||||
_, sidebarWidth, _, _ := m.layout()
|
||||
rows := strings.Split(ansi.Strip(m.sidebarView(sidebarWidth, m.height)), "\n")
|
||||
for _, panel := range []sidebarPanel{panelAgents, panelFindings, panelMcp} {
|
||||
rect := panelRectOf(t, m, panel)
|
||||
header, blank, first := rows[rect.top+1], rows[rect.top+2], rows[rect.top+3]
|
||||
if !strings.Contains(header, m.panelTitle(panel)) || strings.TrimSpace(strings.Trim(blank, "│")) != "" || strings.TrimSpace(strings.Trim(first, "│")) == "" {
|
||||
t.Fatalf("panel %d should render header, blank row, content:\n%s", panel, strings.Join(rows[rect.top:rect.top+4], "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLonePanelHasNoControls(t *testing.T) {
|
||||
m := panelsModel(t)
|
||||
m.snapshot.Vulnerabilities = nil
|
||||
m.snapshot.Connections = nil
|
||||
_, sidebarWidth, _, _ := m.layout()
|
||||
view := ansi.Strip(m.sidebarView(sidebarWidth, m.height))
|
||||
if strings.ContainsAny(view, "▾▸⤢⤡") || !strings.Contains(view, "Agents (31)") {
|
||||
t.Fatalf("a lone Agents panel should carry no collapse or zoom controls:\n%s", view)
|
||||
}
|
||||
rect := panelRectOf(t, m, panelAgents)
|
||||
m = click(t, m, m.width-3, rect.top+1)
|
||||
m = click(t, m, m.width-10, rect.top+1)
|
||||
if m.zoomedPanel != panelNone || len(m.collapsedPanels) != 0 || panelRectOf(t, m, panelAgents) != rect {
|
||||
t.Fatalf("header clicks changed a lone panel: zoom=%v collapsed=%v", m.zoomedPanel, m.collapsedPanels)
|
||||
}
|
||||
if m.sidebarGap() != 0 {
|
||||
t.Fatalf("lone Agents panel should take the spare rows, gap=%d", m.sidebarGap())
|
||||
}
|
||||
m.snapshot.Connections = panelsModel(t).snapshot.Connections
|
||||
if !strings.Contains(ansi.Strip(m.sidebarView(sidebarWidth, m.height)), "▾ Agents (31)") {
|
||||
t.Fatalf("controls should return once a second panel exists")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -544,7 +544,7 @@ func (m Model) sidebarView(width, height int) string {
|
|||
Render(m.viewerBox(width - 4))}
|
||||
agents := ""
|
||||
if agentHeight > 1 {
|
||||
agentRows := max(1, agentHeight-3)
|
||||
agentRows := max(1, agentHeight-4)
|
||||
agents = withVerticalScrollbar(
|
||||
m.agentsView(max(1, width-5), agentRows),
|
||||
width-4,
|
||||
|
|
@ -559,7 +559,7 @@ func (m Model) sidebarView(width, height int) string {
|
|||
if vulnHeight > 0 {
|
||||
findings := ""
|
||||
if vulnHeight > 1 {
|
||||
vulnRows := max(1, vulnHeight-3)
|
||||
vulnRows := max(1, vulnHeight-4)
|
||||
totalRows, offsetRows := m.vulnerabilityScrollRows()
|
||||
findings = withVerticalScrollbar(
|
||||
m.vulnerabilitiesView(m.vulnerabilityListWidth(), vulnRows),
|
||||
|
|
@ -576,7 +576,7 @@ func (m Model) sidebarView(width, height int) string {
|
|||
if mcpHeight > 0 {
|
||||
roster := ""
|
||||
if mcpHeight > 1 {
|
||||
roster = m.mcpConnectionsView(width-4, max(1, mcpHeight-3))
|
||||
roster = m.mcpConnectionsView(width-4, max(1, mcpHeight-4))
|
||||
}
|
||||
parts = append(parts, m.panelBox(panelMcp, roster, width, mcpHeight, m.focus == focusMcp))
|
||||
}
|
||||
|
|
@ -598,22 +598,25 @@ func (m Model) sidebarHeights() (statsHeight, vulnHeight, mcpHeight, agentHeight
|
|||
statsRows := lipgloss.Height(lipgloss.NewStyle().Width(m.sidebarInnerWidth()).Render(m.statsView()))
|
||||
statsHeight = m.panelHeight(panelStats, min(15, statsRows+2))
|
||||
if len(m.snapshot.Vulnerabilities) > 0 {
|
||||
vulnHeight = m.panelHeight(panelFindings, min(12, len(m.vulnerabilityRows(m.vulnerabilityListWidth()))+3))
|
||||
vulnHeight = m.panelHeight(panelFindings, min(13, len(m.vulnerabilityRows(m.vulnerabilityListWidth()))+4))
|
||||
}
|
||||
// Header line + one line per connection + the box border (2). Capped so a
|
||||
// long roster cannot crowd out the agent tree; a roster past the cap scrolls
|
||||
// inside the panel. Absent entirely when the run has no MCP connections.
|
||||
if len(m.snapshot.Connections) > 0 {
|
||||
mcpHeight = m.panelHeight(panelMcp, min(9, len(m.snapshot.Connections)+3))
|
||||
mcpHeight = m.panelHeight(panelMcp, min(10, len(m.snapshot.Connections)+4))
|
||||
}
|
||||
agentHeight = m.panelHeight(panelAgents, 4)
|
||||
for _, h := range []*int{&statsHeight, &mcpHeight, &vulnHeight} {
|
||||
agentHeight = m.panelHeight(panelAgents, 5)
|
||||
for _, p := range []struct {
|
||||
h *int
|
||||
floor int
|
||||
}{{&statsHeight, 4}, {&mcpHeight, 5}, {&vulnHeight, 5}} {
|
||||
over := m.viewerHeight() + statsHeight + vulnHeight + mcpHeight + agentHeight - m.height
|
||||
if over <= 0 {
|
||||
break
|
||||
}
|
||||
if *h > 4 {
|
||||
*h -= min(over, *h-4)
|
||||
if *p.h > p.floor {
|
||||
*p.h -= min(over, *p.h-p.floor)
|
||||
}
|
||||
}
|
||||
for _, h := range []*int{&mcpHeight, &vulnHeight, &statsHeight} {
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ func clampVulnerabilityOffset(offset, total, height int) int {
|
|||
|
||||
func (m Model) vulnerabilityPageSize() int {
|
||||
_, vulnHeight, _, _ := m.sidebarHeights()
|
||||
return max(1, vulnHeight-3)
|
||||
return max(1, vulnHeight-4)
|
||||
}
|
||||
|
||||
// vulnerabilityPageItems is how many findings a page step should move by: the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue