From c86a92d9cbc999cc1bfb39ebb29a002583d14633 Mon Sep 17 00:00:00 2001 From: Christopher Collins Date: Tue, 5 May 2026 12:27:05 -1000 Subject: [PATCH] Fix table viewport panic on small terminal windows (#141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The table height calculation subtracts 35 lines of overhead from the terminal height, producing a negative value for any terminal under 36 lines (including standard 24-line terminals). This was clamped to 1, but table.SetHeight(1) internally subtracts the 2-line header height (text + bottom border), giving the viewport a height of -1. This caused either invisible table rows or a panic in viewport.visibleLines ("slice bounds out of range [2:1]") depending on timing. Raise the minimum tableHeight from 1 to 4 so the viewport always gets at least 2 lines of content space after the header is subtracted. Add TestWindowSizeMsgHandler_SmallWindow covering standard 80x24, tiny, minimum, and zero-height terminals. Created with assistance from Claude 🤖 Signed-off-by: Christopher Collins --- pkg/tui/model_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++ pkg/tui/msgHandlers.go | 8 ++++--- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/pkg/tui/model_test.go b/pkg/tui/model_test.go index d70ab1c..51f9232 100644 --- a/pkg/tui/model_test.go +++ b/pkg/tui/model_test.go @@ -908,3 +908,53 @@ func TestSelectedIncidentSurvivesListUpdate(t *testing.T) { assert.Equal(t, "Original Title", m.selectedIncident.Title, "selectedIncident should retain original data after list reallocation") } + +func TestWindowSizeMsgHandler_SmallWindow(t *testing.T) { + tests := []struct { + name string + width int + height int + }{ + {"standard 80x24 terminal", 80, 24}, + {"tiny window", 40, 10}, + {"minimum height", 20, 1}, + {"zero height", 80, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := model{ + table: newTableWithStyles(), + actionLogTable: newActionLogTable(), + incidentViewer: newIncidentViewer(), + help: newHelp(), + incidentCache: make(map[string]*cachedIncidentData), + } + + // First call sets columns (simulates initial startup WindowSizeMsg) + normalSize := tea.WindowSizeMsg{Width: 120, Height: 50} + result, _ := m.windowSizeMsgHandler(normalSize) + m = result.(model) + + // Incidents arrive and populate the table + m.table.SetRows([]table.Row{ + {".", "P123ABC", "Test incident", "test-service"}, + {".", "P456DEF", "Another incident", "test-service-2"}, + {".", "P789GHI", "Third incident", "test-service-3"}, + }) + + // Second call with small window: columns are already set, so + // table.SetHeight will subtract a 2-line header from the height. + // Before the fix, this panicked with "slice bounds out of range" + // when the resulting viewport height went negative. + msg := tea.WindowSizeMsg{Width: tt.width, Height: tt.height} + assert.NotPanics(t, func() { + result, _ = m.windowSizeMsgHandler(msg) + }) + + m = result.(model) + assert.GreaterOrEqual(t, m.table.Height(), 1, + "viewport height must be positive to avoid panic in visibleLines") + }) + } +} diff --git a/pkg/tui/msgHandlers.go b/pkg/tui/msgHandlers.go index 7b0e764..d308710 100644 --- a/pkg/tui/msgHandlers.go +++ b/pkg/tui/msgHandlers.go @@ -81,9 +81,11 @@ func (m model) windowSizeMsgHandler(msg tea.Msg) (tea.Model, tea.Cmd) { actionLogReservedLines = 11 } tableHeight := windowSize.Height - verticalScratchWidth - tableVerticalScratchWidth - rowCount - estimatedExtraLinesFromComponents - actionLogReservedLines - inputReservedLines - additionalSpacing - // Ensure table height is never negative (can happen with very small terminal windows) - if tableHeight < 1 { - tableHeight = 1 + // table.SetHeight subtracts the rendered header height (2 lines: text + bottom border) + // from the value we pass, so the minimum must exceed the header height to keep the + // internal viewport height positive and avoid a panic in viewport.visibleLines + if tableHeight < 4 { + tableHeight = 4 } m.table.SetHeight(tableHeight)