mirror of
https://github.com/usestrix/strix.git
synced 2026-10-13 03:57:59 +00:00
fix(tui): give the targets box only rows the agents panel would get, and always count hidden targets
This commit is contained in:
parent
710fe7b3b6
commit
442b3c3df5
3 changed files with 75 additions and 20 deletions
|
|
@ -388,3 +388,44 @@ func TestTargetsBoxDropsOnShortTerminal(t *testing.T) {
|
|||
t.Fatalf("sidebar renders %d rows on a %d-row terminal", got, m.height)
|
||||
}
|
||||
}
|
||||
|
||||
// The targets box only takes rows the agent tree would otherwise grow into, so
|
||||
// the stats panel keeps the Caido URL and the other panels keep their heights.
|
||||
func TestTargetsBoxNeverSqueezesPanels(t *testing.T) {
|
||||
for height := 19; height <= 40; height++ {
|
||||
m := panelsModel(t)
|
||||
m.height = height
|
||||
m.snapshot.Usage = map[string]any{"total_tokens": 1234, "cost": 0.5}
|
||||
m.snapshot.CaidoURL = "http://127.0.0.1:8080"
|
||||
m.resizeViewport()
|
||||
stats, vuln, mcp, _ := m.sidebarHeights()
|
||||
m.snapshot.Targets = []string{"a", "b", "c", "d", "e"}
|
||||
m.snapshot.TargetCount = 5
|
||||
m.resizeViewport()
|
||||
gotStats, gotVuln, gotMcp, _ := m.sidebarHeights()
|
||||
if gotStats != stats || gotVuln != vuln || gotMcp != mcp {
|
||||
t.Fatalf("height %d: targets squeezed panels: stats %d->%d findings %d->%d mcp %d->%d",
|
||||
height, stats, gotStats, vuln, gotVuln, mcp, gotMcp)
|
||||
}
|
||||
_, sidebarWidth, _, _ := m.layout()
|
||||
if got := lipgloss.Height(m.sidebarView(sidebarWidth, m.height)); got != m.height {
|
||||
t.Fatalf("height %d: sidebar renders %d rows", height, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetsViewCountsTheTargetsItCannotShow(t *testing.T) {
|
||||
m := panelsModel(t)
|
||||
m.snapshot.Targets = []string{"a", "b", "c", "d", "e"}
|
||||
m.snapshot.TargetCount = 5
|
||||
for rows, want := range map[int]string{
|
||||
2: "Targets 5\n+5 more",
|
||||
3: "Targets 5\n▸ a\n+4 more",
|
||||
6: "Targets 5\n▸ a\n▸ b\n▸ c\n▸ d\n+1 more",
|
||||
9: "Targets 5\n▸ a\n▸ b\n▸ c\n▸ d\n+1 more",
|
||||
} {
|
||||
if got := ansi.Strip(m.targetsView(20, rows)); got != want {
|
||||
t.Fatalf("%d rows:\n%s\nwant:\n%s", rows, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ func (m Model) setupComposer(width int) string {
|
|||
inner := max(1, width-4)
|
||||
body := m.highlightInputSelection(m.input.View())
|
||||
body += "\n\n" + m.setupSummaryView(inner)
|
||||
if targets := m.targetsView(inner); targets != "" {
|
||||
if targets := m.targetsView(inner, maxTargetRows); targets != "" {
|
||||
body += "\n" + targets
|
||||
}
|
||||
return lipgloss.NewStyle().Width(max(1, width-2)).Padding(0, 1).
|
||||
|
|
@ -454,15 +454,22 @@ func (m Model) setupSummaryView(width int) string {
|
|||
return truncate(strings.Join(chips, render.Dim().Render(" · ")), max(1, width))
|
||||
}
|
||||
|
||||
// targetsView lists what the scan is pointed at, once anything is queued.
|
||||
func (m Model) targetsView(width int) string {
|
||||
if len(m.snapshot.Targets) == 0 {
|
||||
// maxTargetRows fits the header, four targets and the count of the rest.
|
||||
const maxTargetRows = 6
|
||||
|
||||
// targetsView lists what the scan is pointed at, once anything is queued, in at
|
||||
// most maxRows rows. Targets that do not fit are counted on the last row.
|
||||
func (m Model) targetsView(width, maxRows int) string {
|
||||
if len(m.snapshot.Targets) == 0 || maxRows < 2 {
|
||||
return ""
|
||||
}
|
||||
const visible = 4
|
||||
total := max(m.snapshot.TargetCount, len(m.snapshot.Targets))
|
||||
visible := min(len(m.snapshot.Targets), maxTargetRows-2, maxRows-1)
|
||||
if visible < total {
|
||||
visible = min(visible, maxRows-2)
|
||||
}
|
||||
rows := []string{render.Bold(green).Render("Targets") + render.Dim().Render(fmt.Sprintf(" %d", total))}
|
||||
for _, target := range m.snapshot.Targets[:min(visible, len(m.snapshot.Targets))] {
|
||||
for _, target := range m.snapshot.Targets[:visible] {
|
||||
rows = append(rows, render.Col(dim).Render("▸ ")+render.Col(white).Render(truncate(target, max(1, width-2))))
|
||||
}
|
||||
if hidden := total - visible; hidden > 0 {
|
||||
|
|
|
|||
|
|
@ -549,7 +549,7 @@ func (m Model) sidebarView(width, height int) string {
|
|||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(dark).
|
||||
Padding(0, 1).
|
||||
Render(fixedPanelBody(m.targetsView(width-4), width-4, targetsHeight-2)))
|
||||
Render(m.targetsView(width-4, targetsHeight-2)))
|
||||
}
|
||||
agents := ""
|
||||
if agentHeight > 1 {
|
||||
|
|
@ -601,6 +601,13 @@ func (m Model) sidebarView(width, height int) string {
|
|||
}
|
||||
|
||||
func (m Model) sidebarHeights() (statsHeight, vulnHeight, mcpHeight, agentHeight int) {
|
||||
_, statsHeight, vulnHeight, mcpHeight, agentHeight = m.sidebarLayout()
|
||||
return
|
||||
}
|
||||
|
||||
// sidebarLayout sizes the targets box and the panels. The targets box only
|
||||
// takes rows no panel needs, so it never squeezes the panels below it.
|
||||
func (m Model) sidebarLayout() (targetsHeight, statsHeight, vulnHeight, mcpHeight, agentHeight int) {
|
||||
// Measure the stats panel the way its box will render it: a long model name
|
||||
// wraps inside the sidebar, and counting only its newlines would size the
|
||||
// box short and push the whole frame past the bottom of the terminal.
|
||||
|
|
@ -620,7 +627,7 @@ func (m Model) sidebarHeights() (statsHeight, vulnHeight, mcpHeight, agentHeight
|
|||
h *int
|
||||
floor int
|
||||
}{{&statsHeight, 4}, {&mcpHeight, 5}, {&vulnHeight, 5}} {
|
||||
over := m.sidebarTopHeight() + statsHeight + vulnHeight + mcpHeight + agentHeight - m.height
|
||||
over := m.viewerHeight() + statsHeight + vulnHeight + mcpHeight + agentHeight - m.height
|
||||
if over <= 0 {
|
||||
break
|
||||
}
|
||||
|
|
@ -629,14 +636,22 @@ func (m Model) sidebarHeights() (statsHeight, vulnHeight, mcpHeight, agentHeight
|
|||
}
|
||||
}
|
||||
for _, h := range []*int{&mcpHeight, &vulnHeight, &statsHeight} {
|
||||
if m.sidebarTopHeight()+statsHeight+vulnHeight+mcpHeight+agentHeight <= m.height {
|
||||
if m.viewerHeight()+statsHeight+vulnHeight+mcpHeight+agentHeight <= m.height {
|
||||
break
|
||||
}
|
||||
if *h > 1 {
|
||||
*h = 1
|
||||
}
|
||||
}
|
||||
spare := max(0, m.height-m.sidebarTopHeight()-statsHeight-vulnHeight-mcpHeight-agentHeight)
|
||||
spare := max(0, m.height-m.viewerHeight()-statsHeight-vulnHeight-mcpHeight-agentHeight)
|
||||
// The box needs its border and two rows: the header and a target or the
|
||||
// count of hidden ones. A zoomed panel keeps the whole sidebar.
|
||||
if targets := m.targetsView(m.sidebarInnerWidth(), maxTargetRows); targets != "" && m.zoomedPanel == panelNone {
|
||||
if fit := min(lipgloss.Height(targets)+2, spare); fit >= 4 {
|
||||
targetsHeight = fit
|
||||
spare -= fit
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case m.zoomedPanel == panelFindings && vulnHeight > 1:
|
||||
vulnHeight += spare
|
||||
|
|
@ -658,17 +673,9 @@ func (m Model) sidebarTopHeight() int {
|
|||
return m.viewerHeight() + m.targetsBoxHeight()
|
||||
}
|
||||
|
||||
// targetsBoxHeight drops the box on terminals too short to also fit the agents
|
||||
// box, the stats box at its floor and squeezed findings and MCP headers.
|
||||
func (m Model) targetsBoxHeight() int {
|
||||
if len(m.snapshot.Targets) == 0 {
|
||||
return 0
|
||||
}
|
||||
room := m.height - m.viewerHeight() - 5 - 4 - 2
|
||||
if room < 3 {
|
||||
return 0
|
||||
}
|
||||
return min(lipgloss.Height(m.targetsView(m.sidebarInnerWidth()))+2, room)
|
||||
targetsHeight, _, _, _, _ := m.sidebarLayout()
|
||||
return targetsHeight
|
||||
}
|
||||
|
||||
func (m Model) sidebarInnerWidth() int {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue