diff --git a/internal/cli/tui.go b/internal/cli/tui.go index 518fbf0..1fca59e 100644 --- a/internal/cli/tui.go +++ b/internal/cli/tui.go @@ -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 } diff --git a/internal/cli/tui_test.go b/internal/cli/tui_test.go index d3138a4..eca285b 100644 --- a/internal/cli/tui_test.go +++ b/internal/cli/tui_test.go @@ -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",