From 38e1fe0493325283128519f36746ffdc7aa30894 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 Jul 2026 11:56:47 +0200 Subject: [PATCH 1/3] Add tests showing ghost views when branches/commits are not their panel's first tab With gui.sidePanels, a panel's gocui window is named after its first tab. The transient contexts (remoteBranches, subCommits, commitFiles) initially point at the windows "branches" and "commits"; when the config gives no panel that name, their views end up visible at full screen size, covering every side panel below them in z-order (issue #5823). Co-Authored-By: Claude Fable 5 --- pkg/integration/tests/test_list.go | 2 ++ .../tests/ui/branches_not_first_tab.go | 34 +++++++++++++++++++ .../tests/ui/commits_not_first_tab.go | 32 +++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 pkg/integration/tests/ui/branches_not_first_tab.go create mode 100644 pkg/integration/tests/ui/commits_not_first_tab.go diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index abf13073e..8213d5159 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -484,6 +484,8 @@ var tests = []*components.IntegrationTest{ tag.Reset, tag.ResetToDuplicateNamedBranch, ui.Accordion, + ui.BranchesNotFirstTab, + ui.CommitsNotFirstTab, ui.DisableSwitchTabWithPanelJumpKeys, ui.EmptyMenu, ui.HideSidePanel, diff --git a/pkg/integration/tests/ui/branches_not_first_tab.go b/pkg/integration/tests/ui/branches_not_first_tab.go new file mode 100644 index 000000000..0e60b5ed9 --- /dev/null +++ b/pkg/integration/tests/ui/branches_not_first_tab.go @@ -0,0 +1,34 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var BranchesNotFirstTab = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "With gui.sidePanels grouping branches behind another tab, no ghost view must appear over the side panels", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.SidePanels = []config.SidePanel{ + {"worktrees", "branches", "remotes"}, + {"files"}, + {"commits", "tags"}, + {"stash"}, + } + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // The remote branches and sub-commits views are only shown after + // drilling into a remote or a branch; at startup both must be hidden, + // or they'd cover the side panels. + t.Views().RemoteBranches(). + /* EXPECTED: + IsInvisible() + ACTUAL: */ + IsVisible() + t.Views().SubCommits().IsInvisible() + }, +}) diff --git a/pkg/integration/tests/ui/commits_not_first_tab.go b/pkg/integration/tests/ui/commits_not_first_tab.go new file mode 100644 index 000000000..db50e4c08 --- /dev/null +++ b/pkg/integration/tests/ui/commits_not_first_tab.go @@ -0,0 +1,32 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var CommitsNotFirstTab = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "With gui.sidePanels grouping commits behind another tab, no ghost view must appear over the side panels", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.SidePanels = []config.SidePanel{ + {"branches", "worktrees", "remotes"}, + {"files"}, + {"tags", "commits"}, + {"stash"}, + } + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // The commit files view is only shown after drilling into a commit; at + // startup it must be hidden, or it'd cover the side panels. + t.Views().CommitFiles(). + /* EXPECTED: + IsInvisible() + ACTUAL: */ + IsVisible() + }, +}) From bf4f5827e748574b9267520f48856e2c5ff7f049 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 Jul 2026 12:00:00 +0200 Subject: [PATCH 2/3] Don't show a transient view whose window is not part of the layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With gui.sidePanels, a panel's gocui window is named after its first tab, so when branches is grouped behind, say, worktrees, there is no window called "branches" at all. The transient contexts (remoteBranches, subCommits, commitFiles) initially point at the windows "branches" and "commits", and layout() showed their views whenever the window-to-view map named them as their window's current view — without checking that the window exists in the layout. Since the map is seeded from the contexts themselves, a window that no panel owns keeps naming a transient view as its current view, and that view had just been parked at full screen size (the fallback for views in unlaid-out windows), so it covered every side panel below it in z-order. Only show a transient view if its window actually received dimensions in this layout. Fixes #5823. Co-Authored-By: Claude Fable 5 --- pkg/gui/layout.go | 9 ++++++++- pkg/integration/tests/ui/branches_not_first_tab.go | 3 --- pkg/integration/tests/ui/commits_not_first_tab.go | 3 --- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index de3bdbe9b..bcdc0edfc 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -154,7 +154,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != nil && !errors.Is(err, gocui.ErrUnknownView) { return err } - view.Visible = gui.helpers.Window.GetViewNameForWindow(context.GetWindowName()) == context.GetViewName() + // A transient view is visible if it is the view its window is currently + // showing — but only if that window is part of the layout at all. For a + // window without dimensions, setViewFromDimensions parks the view at full + // screen size in the background, so making it visible would cover all + // windows below it. + _, windowHasDimensions := viewDimensions[context.GetWindowName()] + view.Visible = windowHasDimensions && + gui.helpers.Window.GetViewNameForWindow(context.GetWindowName()) == context.GetViewName() } if gui.PrevLayout.Information != informationStr { diff --git a/pkg/integration/tests/ui/branches_not_first_tab.go b/pkg/integration/tests/ui/branches_not_first_tab.go index 0e60b5ed9..47e8a4fd5 100644 --- a/pkg/integration/tests/ui/branches_not_first_tab.go +++ b/pkg/integration/tests/ui/branches_not_first_tab.go @@ -25,10 +25,7 @@ var BranchesNotFirstTab = NewIntegrationTest(NewIntegrationTestArgs{ // drilling into a remote or a branch; at startup both must be hidden, // or they'd cover the side panels. t.Views().RemoteBranches(). - /* EXPECTED: IsInvisible() - ACTUAL: */ - IsVisible() t.Views().SubCommits().IsInvisible() }, }) diff --git a/pkg/integration/tests/ui/commits_not_first_tab.go b/pkg/integration/tests/ui/commits_not_first_tab.go index db50e4c08..505aa4307 100644 --- a/pkg/integration/tests/ui/commits_not_first_tab.go +++ b/pkg/integration/tests/ui/commits_not_first_tab.go @@ -24,9 +24,6 @@ var CommitsNotFirstTab = NewIntegrationTest(NewIntegrationTestArgs{ // The commit files view is only shown after drilling into a commit; at // startup it must be hidden, or it'd cover the side panels. t.Views().CommitFiles(). - /* EXPECTED: IsInvisible() - ACTUAL: */ - IsVisible() }, }) From 74a77e58be5f8c478481fe4ee3b1ce6de3351600 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 Jul 2026 12:02:23 +0200 Subject: [PATCH 3/3] Assign the transient contexts' initial windows from the side panel config The transient contexts (remoteBranches, subCommits, commitFiles) take over the window of the context they are drilled into from, but until then they carry a hardcoded initial window ("branches" or "commits"). Under a gui.sidePanels config where those tabs aren't their panel's first, no window of that name exists, leaving the window-to-view map with entries for windows the layout never produces. The previous commit made such entries harmless, but there's no reason to have contexts point at nonexistent windows in the first place; assign them the window hosting branches or commits instead, which the config validation guarantees to exist. Co-Authored-By: Claude Fable 5 --- pkg/gui/side_panels.go | 9 +++++++++ pkg/gui/side_panels_test.go | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pkg/gui/side_panels.go b/pkg/gui/side_panels.go index 361d54fb1..8b327b197 100644 --- a/pkg/gui/side_panels.go +++ b/pkg/gui/side_panels.go @@ -125,4 +125,13 @@ func (gui *Gui) assignSidePanelWindows(contextTree *context.ContextTree) { ctx.SetWindowName(name) } } + + // The transient contexts take over the window of the context they are + // drilled into from, but they need a valid initial window before their + // first use. Assign the window hosting branches or commits, respectively; + // unlike e.g. remotes, those tabs can't be hidden, so their windows are + // always part of the layout. + contextTree.RemoteBranches.SetWindowName(contextTree.Branches.GetWindowName()) + contextTree.SubCommits.SetWindowName(contextTree.Branches.GetWindowName()) + contextTree.CommitFiles.SetWindowName(contextTree.LocalCommits.GetWindowName()) } diff --git a/pkg/gui/side_panels_test.go b/pkg/gui/side_panels_test.go index b1240c357..24813b4ae 100644 --- a/pkg/gui/side_panels_test.go +++ b/pkg/gui/side_panels_test.go @@ -28,3 +28,23 @@ func TestSidePanelLookupsCoverAllValidTabs(t *testing.T) { assert.Equal(t, want, sortedKeys(gui.sidePanelTabTitles())) assert.Equal(t, want, sortedKeys(sidePanelContexts(gui.contextTree()))) } + +// The transient contexts must end up in windows that exist under the configured +// panel layout, or their views would be laid out for a window that is never +// shown. +func TestAssignSidePanelWindowsCoversTransientContexts(t *testing.T) { + gui := NewDummyGui() + gui.c.UserConfig().Gui.SidePanels = []config.SidePanel{ + {"worktrees", "branches", "remotes"}, + {"files"}, + {"tags", "commits"}, + {"stash"}, + } + + contextTree := gui.contextTree() + gui.assignSidePanelWindows(contextTree) + + assert.Equal(t, "worktrees", contextTree.RemoteBranches.GetWindowName()) + assert.Equal(t, "worktrees", contextTree.SubCommits.GetWindowName()) + assert.Equal(t, "tags", contextTree.CommitFiles.GetWindowName()) +}