fix(tui): clamp member scroll and footer width

This commit is contained in:
Vincent Koc 2026-04-29 18:45:19 -07:00
parent b2a52f4418
commit fa3e56e1db
No known key found for this signature in database
2 changed files with 77 additions and 14 deletions

View File

@ -525,8 +525,9 @@ func (m clusterBrowserModel) renderHeader(width int) string {
if m.payload.InferredRepository {
line += " inferred"
}
style := lipgloss.NewStyle().Width(width).Height(1).Background(lipgloss.Color("#0d1321")).Foreground(lipgloss.Color("#f7f7ff")).Bold(true).Padding(0, 1)
return style.Render(truncateCells(line, maxInt(1, width-2)))
content := padCells(" "+truncateCells(line, maxInt(1, width-2)), width)
style := lipgloss.NewStyle().Width(width).Height(1).Background(lipgloss.Color("#0d1321")).Foreground(lipgloss.Color("#f7f7ff")).Bold(true)
return style.Render(content)
}
func (m clusterBrowserModel) renderFooter(width int) string {
@ -548,7 +549,9 @@ func (m clusterBrowserModel) renderFooter(width int) string {
line = strings.TrimSpace(line + " " + location)
}
bg, fg := footerPalette(m.payload.DBSource)
return lipgloss.NewStyle().Width(width).Height(2).Background(bg).Foreground(fg).Padding(0, 1).Render(truncateCells(line, width-2) + "\n" + truncateCells(controls, maxInt(1, width-2)))
statusLine := padCells(" "+truncateCells(line, maxInt(1, width-2)), width)
controlsLine := padCells(" "+truncateCells(controls, maxInt(1, width-2)), width)
return lipgloss.NewStyle().Width(width).Height(2).Background(bg).Foreground(fg).Render(statusLine + "\n" + controlsLine)
}
func loadingFrame(index int) string {
@ -3085,18 +3088,14 @@ func (m clusterBrowserModel) nextSelectableMemberIndex(current, delta int) int {
}
index := current
for moved := 0; moved < steps; moved++ {
for attempts := 0; attempts < len(m.memberRows); attempts++ {
index += step
if index < 0 {
index = len(m.memberRows) - 1
}
if index >= len(m.memberRows) {
index = 0
}
if m.memberRows[index].selectable {
break
}
next := index + step
for next >= 0 && next < len(m.memberRows) && !m.memberRows[next].selectable {
next += step
}
if next < 0 || next >= len(m.memberRows) {
return index
}
index = next
}
return index
}

View File

@ -76,6 +76,22 @@ func TestTUIHeaderShowsDetailMode(t *testing.T) {
}
}
func TestTUIHeaderDoesNotWrapAtTerminalWidth(t *testing.T) {
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
Repository: strings.Repeat("openclaw/", 20),
Sort: "recent",
Clusters: sampleTUIClusters(),
})
header := model.renderHeader(80)
lines := strings.Split(header, "\n")
if len(lines) != 1 {
t.Fatalf("header rendered %d lines, want 1:\n%s", len(lines), header)
}
if width := lipgloss.Width(lines[0]); width > 80 {
t.Fatalf("header width = %d, want <= 80: %q", width, lines[0])
}
}
func TestTUIViewKeepsEssentialFooterHintsNarrow(t *testing.T) {
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
Repository: "openclaw/openclaw",
@ -151,6 +167,31 @@ func TestTUIFooterShowsRemoteRefreshLoadingState(t *testing.T) {
}
}
func TestTUIFooterDoesNotWrapLongRemoteLocation(t *testing.T) {
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
Repository: "openclaw/openclaw",
DBSource: "remote",
DBLocation: "openclaw/gitcrawl-store:" + strings.Repeat("openclaw__openclaw.sync.db", 6),
Sort: "recent",
Clusters: sampleTUIClusters(),
})
model.status = "Cluster 14316"
footer := model.renderFooter(80)
lines := strings.Split(footer, "\n")
if len(lines) != 2 {
t.Fatalf("footer rendered %d lines, want 2:\n%s", len(lines), footer)
}
if !strings.Contains(lines[1], "? help") || !strings.Contains(lines[1], "q quit") {
t.Fatalf("footer controls were displaced:\n%s", footer)
}
for index, line := range lines {
if width := lipgloss.Width(line); width > 80 {
t.Fatalf("footer line %d width = %d, want <= 80: %q", index, width, line)
}
}
}
func TestTUIViewFitsTerminalFrame(t *testing.T) {
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
Repository: "openclaw/openclaw",
@ -517,6 +558,29 @@ func TestTUIWideLayoutToggle(t *testing.T) {
}
}
func TestTUIMemberMovementDoesNotWrapPastEdges(t *testing.T) {
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
Repository: "openclaw/openclaw",
Sort: "recent",
Clusters: sampleTUIClusters(),
})
model.memberRows = []memberRow{
{label: "ISSUES (2)"},
{selectable: true, member: store.ClusterMemberDetail{Thread: store.Thread{Number: 1, State: "open"}}},
{selectable: true, member: store.ClusterMemberDetail{Thread: store.Thread{Number: 2, State: "open"}}},
}
if got := model.nextSelectableMemberIndex(2, 1); got != 2 {
t.Fatalf("member down from last = %d, want last row", got)
}
if got := model.nextSelectableMemberIndex(1, -1); got != 1 {
t.Fatalf("member up from first = %d, want first row", got)
}
if got := model.nextSelectableMemberIndex(1, 10); got != 2 {
t.Fatalf("member page down = %d, want last row", got)
}
}
func TestTUIRightClickOpensFloatingMenu(t *testing.T) {
model := newClusterBrowserModel(context.Background(), nil, 0, clusterBrowserPayload{
Repository: "openclaw/openclaw",