From 7debcdfd155070ade8bf73417514e6cc03033fb9 Mon Sep 17 00:00:00 2001 From: jdidion Date: Mon, 27 Jul 2026 10:33:46 -0700 Subject: [PATCH 1/3] feat(ui): route mouse-wheel to the preview in stacked (below) layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit review on #1626 (merged): in the below/stacked preview layout, the mouse wheel always moved the session-list cursor because wheel-to-preview routing was dual-layout-only (X-based: "is the wheel over the right column"). Stacked mode has no right column — the preview is the LOWER region — so scroll input never reached it. Add Y-based routing for stacked mode: a wheel event at or below the horizontal divider scrolls the preview (previewScrollOffset), matching the dual layout's X-based behaviour. New stackedPreviewTopY() computes the divider Y by mirroring renderStackedLayout's height split (via the shared stackedListHeight helper + a new contentChromeTop() that both this and the render path use, so the Y can't drift from what's drawn). Single layout unchanged (no preview target). Scope: this addresses the wheel-scroll half of the review. Divider RESIZE in stacked mode stays on the `<`/`>` keys (added in #1626) rather than a mouse drag — the X-axis divider-drag path (isOnDivider/setPreviewPctFromMouseX) is X-only, and a parallel Y-drag would duplicate fragile layout math for redundant polish. Noted for a possible follow-up. Build + go vet + gofmt clean; UI layout/preview tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/ui/home.go | 74 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/internal/ui/home.go b/internal/ui/home.go index e9eb43c5..f70db828 100644 --- a/internal/ui/home.go +++ b/internal/ui/home.go @@ -1068,6 +1068,57 @@ func (h *Home) getLayoutMode() string { } } +// contentChromeTop returns the number of rows rendered ABOVE the main content +// area (the header line + filter bar + optional update/maintenance banners). +// The debug footer is NOT included — it renders below the content, so it +// doesn't shift the content's top edge. Kept in one place so mouse Y-routing +// and the render path agree on where content starts. +func (h *Home) contentChromeTop() int { + top := 1 // header line + top++ // filter bar (always shown, matches View()) + if h.shouldRenderUpdateNudge() { + top++ + } + if h.maintenanceMsg != "" { + top++ + } + return top +} + +// stackedPreviewTopY returns the screen Y (0-indexed) of the first row of the +// PREVIEW region in the stacked layout, i.e. the row just below the horizontal +// separator. A mouse-wheel event at or below this Y is over the preview and +// should scroll it rather than move the list cursor. Returns -1 when the +// current layout is not stacked. Mirrors renderStackedLayout's height split: +// +// [content-top] SESSIONS title (panelTitleLines) + list (listHeight-title) +// + separator (1) <- divider +// [preview-top] PREVIEW title + preview content +func (h *Home) stackedPreviewTopY() int { + if h.getLayoutMode() != LayoutModeStacked { + return -1 + } + const helpBarHeight = 2 + const panelTitleLines = 2 + filterBarHeight := 1 + updateBannerHeight := 0 + if h.shouldRenderUpdateNudge() { + updateBannerHeight = 1 + } + maintenanceBannerHeight := 0 + if h.maintenanceMsg != "" { + maintenanceBannerHeight = 1 + } + debugBarHeight := 0 + if h.debugMode { + debugBarHeight = 1 + } + contentHeight := h.height - 1 - helpBarHeight - updateBannerHeight - maintenanceBannerHeight - filterBarHeight - debugBarHeight + listHeight := h.stackedListHeight(contentHeight) + // content top + full list block (title + body) + the 1-row separator. + return h.contentChromeTop() + listHeight + 1 +} + // Messages type loadSessionsMsg struct { instances []*session.Instance @@ -5098,13 +5149,13 @@ func (h *Home) updateInner(msg tea.Msg) (tea.Model, tea.Cmd) { return h, nil } // Preview pane scroll (#574): when the wheel event lands in the - // preview region of the dual layout, scroll preview content - // instead of moving the list cursor. Other layouts keep the - // legacy list-scroll behaviour because they have no dedicated - // preview click-target (single = no preview; stacked = same- - // width column where Y-based routing is ambiguous enough to - // leave as list-scroll). - if h.getLayoutMode() == LayoutModeDual { + // preview region, scroll preview content instead of moving the + // list cursor. Dual layout routes by X (preview is the right + // column); stacked layout routes by Y (preview is the lower + // region, below the horizontal divider). Single layout has no + // preview target, so it keeps the legacy list-scroll behaviour. + switch h.getLayoutMode() { + case LayoutModeDual: leftWidth := h.sessionsPaneWidth() if msg.X >= leftWidth { if msg.Button == tea.MouseButtonWheelUp { @@ -5114,6 +5165,15 @@ func (h *Home) updateInner(msg tea.Msg) (tea.Model, tea.Cmd) { } return h, nil } + case LayoutModeStacked: + if top := h.stackedPreviewTopY(); top >= 0 && msg.Y >= top { + if msg.Button == tea.MouseButtonWheelUp { + h.previewScrollOffset++ + } else if h.previewScrollOffset > 0 { + h.previewScrollOffset-- + } + return h, nil + } } // Main session list scroll (cursor movement also resets any // stale preview offset so the new session starts at its tail). From 7acaa3d07bee1a707e092f019229846b478c032b Mon Sep 17 00:00:00 2001 From: jdidion Date: Mon, 27 Jul 2026 11:22:37 -0700 Subject: [PATCH 2/3] fix(ui): remove unused panelTitleLines const in stackedPreviewTopY (golangci) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The golangci `unused` linter flagged home.go:1102 — panelTitleLines was declared in stackedPreviewTopY but never used (stackedListHeight already accounts for the panel title internally). Dead code; removed. Verified with golangci-lint run locally (0 issues). Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/ui/home.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/ui/home.go b/internal/ui/home.go index f70db828..668d2d67 100644 --- a/internal/ui/home.go +++ b/internal/ui/home.go @@ -1099,7 +1099,6 @@ func (h *Home) stackedPreviewTopY() int { return -1 } const helpBarHeight = 2 - const panelTitleLines = 2 filterBarHeight := 1 updateBannerHeight := 0 if h.shouldRenderUpdateNudge() { From 8bac290c0483f36051348156a513c3a781cd23d9 Mon Sep 17 00:00:00 2001 From: jdidion Date: Mon, 27 Jul 2026 11:41:42 -0700 Subject: [PATCH 3/3] ci: re-trigger CI (re-roll unrelated flaky -race session test) No code change. The prior push (golangci green) hit a flaky internal/session -race failure (TestIssue1421 / -race timing) unrelated to this PR's one-line home.go change, which passed the Full test suite on the push before it. Empty commit to re-run the gate. Co-Authored-By: Claude Opus 4.8 (1M context)