fix: stretch prototype TUI panels to available space#33
Conversation
There was a problem hiding this comment.
Pull request overview
Advances the Phase 2 prototype TUI layout work by making screen panels size to the available terminal width/height (instead of fixed “islands”), and adds regression tests around the new available-space behavior.
Changes:
- Make prototype screen views render panels at
availableWidth()and (mostly)availableContentHeight(). - Introduce
availableContentHeight()and apanelWithHeight()helper to drive fixed-height panel rendering. - Add layout tests asserting screens use available width/height; update changelog entry to reflect the new behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| internal/tui/app.go | Reworks panel sizing to use available width/height; adds height-aware panel helper and content-height calculation. |
| internal/tui/app_test.go | Adds tests to validate screen views expand to available width and (at least) available content height. |
| CHANGELOG.md | Updates Unreleased notes to mention available-space panel stretching. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| topHeight := max((height-1)/2, 6) | ||
| menuHeight := max(height-topHeight, 6) |
There was a problem hiding this comment.
In the party dashboard layout, the height split currently over-allocates by one line: screenView renders topHeight (top row) + 1 separator newline + menuHeight, but menuHeight is computed as height-topHeight, so the combined height becomes height+1 and can get clipped. Consider computing the bottom section as the remaining space after accounting for the separator (e.g., height-topHeight-1) and revisiting the minimums so small terminals don’t force 6 + 1 + 6 lines when less space is available.
| topHeight := max((height-1)/2, 6) | |
| menuHeight := max(height-topHeight, 6) | |
| separatorHeight := 1 | |
| splitHeight := max(height-separatorHeight, 0) | |
| topHeight := splitHeight / 2 | |
| menuHeight := splitHeight - topHeight | |
| if splitHeight >= 12 { | |
| topHeight = max(topHeight, 6) | |
| menuHeight = max(menuHeight, 6) | |
| if topHeight+menuHeight > splitHeight { | |
| menuHeight = splitHeight - topHeight | |
| } | |
| } |
| width := m.availableWidth() | ||
| height := m.availableContentHeight() | ||
| gap := 1 | ||
| panelWidth := max((width-gap)/2, 24) |
There was a problem hiding this comment.
panelWidth := max((width-gap)/2, 24) can exceed the available width on smaller windows (e.g., availableWidth() can be 40, but 24+1+24 requires 49). This makes the joined panels wider than availableWidth() and breaks the “use available width” goal on narrow terminals. Consider clamping panelWidth to at most (width-gap)/2 (or switching to a vertical stack layout below a threshold).
| panelWidth := max((width-gap)/2, 24) | |
| panelWidth := (width - gap) / 2 |
| func (m Model) availableContentHeight() int { | ||
| if m.height == 0 { | ||
| return 12 | ||
| } | ||
|
|
||
| const chromeHeight = 6 // title, nav, spacer lines, and one-line footer. | ||
| return max(m.height-m.theme.App.GetVerticalFrameSize()-chromeHeight, 8) |
There was a problem hiding this comment.
availableContentHeight() hard-codes chromeHeight = 6 as if the non-content chrome is always 6 lines, but View() currently renders 5 fixed lines when help is hidden (title + nav + 2 spacer blank lines + 1-line footer). Also, when m.showHelp is true the footer becomes a multi-line panel, so the available content height should shrink (or help should overlay/replace content) to avoid the view exceeding the terminal height.
| func (m Model) availableContentHeight() int { | |
| if m.height == 0 { | |
| return 12 | |
| } | |
| const chromeHeight = 6 // title, nav, spacer lines, and one-line footer. | |
| return max(m.height-m.theme.App.GetVerticalFrameSize()-chromeHeight, 8) | |
| func (m Model) contentChromeHeight() int { | |
| const baseChromeHeight = 5 // title, nav, two spacer lines, and one-line footer. | |
| if !m.showHelp { | |
| return baseChromeHeight | |
| } | |
| const extraHelpHeight = 3 // help footer replaces the one-line footer with a multi-line panel. | |
| return baseChromeHeight + extraHelpHeight | |
| } | |
| func (m Model) availableContentHeight() int { | |
| if m.height == 0 { | |
| return 12 | |
| } | |
| return max(m.height-m.theme.App.GetVerticalFrameSize()-m.contentChromeHeight(), 8) |
|
|
||
| for _, screen := range screens { | ||
| model.screen = screen | ||
| require.GreaterOrEqual(t, lipgloss.Height(model.screenView()), model.availableContentHeight(), screen.String()) |
There was a problem hiding this comment.
The height assertion here only checks >= availableContentHeight(), which allows screens to overflow the intended content area (e.g., an off-by-one newline in a composed view). Since the goal is to fit the available space, it would be more robust to assert the rendered height does not exceed the available content height as well (or even equals it for fixed-height panels).
| require.GreaterOrEqual(t, lipgloss.Height(model.screenView()), model.availableContentHeight(), screen.String()) | |
| height := lipgloss.Height(model.screenView()) | |
| availableHeight := model.availableContentHeight() | |
| require.GreaterOrEqual(t, height, availableHeight, screen.String()) | |
| require.LessOrEqual(t, height, availableHeight, screen.String()) |
| width := m.availableWidth() | ||
| height := m.availableContentHeight() | ||
| gap := 1 | ||
| leftWidth := max((width-gap)/2, 24) | ||
| rightWidth := max(width-gap-leftWidth, 24) |
There was a problem hiding this comment.
The commander dashboard’s 2-column width math can exceed availableWidth() on narrow terminals: leftWidth is forced to at least 24 and rightWidth is also forced to at least 24, so the total minimum becomes 24 + 1 + 24 = 49 even though availableWidth() can return as low as 40. Consider clamping the per-column minimum based on width (or falling back to a stacked layout when width < 49).
|
Follow-up from Donovan's terminal screenshot:
Re-verified:
|
|
Documented the Phase 2 theme decision: |
|
Added the implementation-phase note that users should be able to configure their own default TUI theme, analogous to the existing default game workflow. |
|
Addressed Copilot's layout-boundary comments in 7f4df61:
Re-verified:
|
Summary
feat/tui.Test Plan
go test ./...go vet ./...go build -o /tmp/lmm-tui-visual ./cmd/lmm/tmp/lmm-tui-visual tui --prototype --theme wizardry, then quit withqRefs #32