From caef98fa3886317de0122cf51a16077dd8eefe5c Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Sun, 29 Mar 2026 14:34:44 -0400 Subject: [PATCH] fix(tui): preserve last queue entry in distraction-free mode (#586) The lipgloss table drops the last data row when Headers() is not called. Compact mode now always sets (empty) headers and strips the resulting blank line, ensuring all entries remain visible when toggling D. Co-Authored-By: Claude Opus 4.6 --- cmd/roborev/tui/queue_test.go | 40 +++++++++++++++++++++++++++++++++ cmd/roborev/tui/render_queue.go | 14 ++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/cmd/roborev/tui/queue_test.go b/cmd/roborev/tui/queue_test.go index 9ce85f503..36dfe5616 100644 --- a/cmd/roborev/tui/queue_test.go +++ b/cmd/roborev/tui/queue_test.go @@ -347,6 +347,46 @@ func TestTUIQueueDistractionFreeToggle(t *testing.T) { assert.Contains(t, output, "JobID") } +// Regression test for #586: distraction-free mode lost the last queue entry. +// The lipgloss table drops the last data row when Headers() is not called, +// so compact mode must still supply (empty) headers and strip the resulting +// blank line. +func TestTUIQueueDistractionFreePreservesLastJob(t *testing.T) { + assert := assert.New(t) + for _, h := range []int{20, 24, 30} { + for _, n := range []int{10, 25, 40} { + t.Run(fmt.Sprintf("h%d_jobs%d", h, n), func(t *testing.T) { + m := newTuiModel("http://localhost") + m.currentView = tuiViewQueue + m.width = 120 + m.height = h + for i := range n { + m.jobs = append(m.jobs, makeJob(int64(9300+i))) + } + lastJobID := fmt.Sprintf("%d", 9300+n-1) + // Select near the end so the last job is in the scroll window. + m.selectedIdx = n - 2 + m.selectedJobID = int64(9300 + n - 2) + + normalOutput := m.View() + normalHasLast := strings.Contains(normalOutput, lastJobID) + + m2, _ := updateModel(t, m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'D'}}) + compactOutput := m2.View() + compactHasLast := strings.Contains(compactOutput, lastJobID) + + if normalHasLast { + assert.True(compactHasLast, + "h=%d n=%d: job %s visible before D but missing after", h, n, lastJobID) + } + compactLines := strings.Count(compactOutput, "\n") + 1 + assert.LessOrEqual(compactLines, h, + "h=%d n=%d: output %d lines exceeds terminal height", h, n, compactLines) + }) + } + } +} + func TestTUITasksMouseClickSelectsRow(t *testing.T) { m := newTuiModel("http://localhost") m.currentView = tuiViewTasks diff --git a/cmd/roborev/tui/render_queue.go b/cmd/roborev/tui/render_queue.go index 238edfb38..5f913cbef 100644 --- a/cmd/roborev/tui/render_queue.go +++ b/cmd/roborev/tui/render_queue.go @@ -526,16 +526,26 @@ func (m model) renderQueueView() string { return s }) + // Always set headers — lipgloss table drops the last data row + // when Headers() is not called. + headers := make([]string, len(visCols)) if !compact { - headers := make([]string, len(visCols)) for vi, c := range visCols { headers[vi] = allHeaders[c] } - t = t.Headers(headers...) } + t = t.Headers(headers...) t = t.Rows(rows...) tableStr := t.Render() + + // In compact mode, strip the empty header line we added as a + // workaround (it renders as a row of spaces). + if compact { + if idx := strings.Index(tableStr, "\n"); idx >= 0 { + tableStr = tableStr[idx+1:] + } + } b.WriteString(tableStr) b.WriteString("\x1b[K\n")