From 97b1b4cf7f1e171b887cdaf1ce32a5cc0f24de0f Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 6 Jul 2026 09:33:32 +0200 Subject: [PATCH] Offer commit and find-fixup-base in the focused main view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The focused main view now plays the role the staging panel used to, so the working-tree commands the staging panel offered — commit (and its variants) and find-base-commit-for-fixup — need to be reachable there as well. Gate them to when the focused main view actually shows the working-tree diff (DiffMainViewTypeStaging), which is exactly the role the staging panel filled. Over a commit's or stash's diff these commands would operate on the working tree, unrelated to what's on screen, so the keys are a no-op there and the bindings don't clutter its keybinding menu. The gate is re-checked on each press rather than captured at registration time, since one keybinding set serves the main view over every panel. That requires reading the panel beneath the main view from GetKeybindings (to decide whether to show the descriptions), which runs for off-stack contexts too — at startup and during cheatsheet generation — so a panic-safe IsInStack guards the NextInStack lookup. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gui/context.go | 12 ++++ pkg/gui/controllers/main_view_controller.go | 73 +++++++++++++++++++++ pkg/gui/types/context.go | 1 + 3 files changed, 86 insertions(+) diff --git a/pkg/gui/context.go b/pkg/gui/context.go index 1adfec35c..ecab07c76 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -373,3 +373,15 @@ func (self *ContextMgr) NextInStack(c types.Context) types.Context { panic("context not in stack") } + +// IsInStack reports whether the given context is somewhere in the context stack. +// Unlike NextInStack it never panics, so it's safe to call before dereferencing +// the stack from a caller that might run with the context off-stack. +func (self *ContextMgr) IsInStack(c types.Context) bool { + self.RLock() + defer self.RUnlock() + + return lo.ContainsBy(self.ContextStack, func(other types.Context) bool { + return other.GetKey() == c.GetKey() + }) +} diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 8d44aa3f9..9a37b0265 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -45,6 +45,11 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty // line in the branch's pull request). selectionShown := self.context.GetView().Highlight + // The commit and find-fixup-base commands act on the working tree, so — like the + // old staging panel, which was always the working-tree diff — we only surface them + // while the focused main view plays that role (see workingTreeAction). + stagingMode := self.diffMainViewType() == types.DiffMainViewTypeStaging + var enterDescription string var editDescription string var editTooltip string @@ -59,6 +64,21 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty openPullRequestTooltip = "Open a browser at the selected line in the diff of the current branch's pull request, so that you can comment on it. Only works for local branches that have a pull request on GitHub." } + var commitDescription string + var commitTooltip string + var commitWithoutHookDescription string + var commitWithEditorDescription string + var findBaseCommitDescription string + var findBaseCommitTooltip string + if stagingMode { + commitDescription = self.c.Tr.Commit + commitTooltip = self.c.Tr.CommitTooltip + commitWithoutHookDescription = self.c.Tr.CommitChangesWithoutHook + commitWithEditorDescription = self.c.Tr.CommitChangesWithEditor + findBaseCommitDescription = self.c.Tr.FindBaseCommitForFixup + findBaseCommitTooltip = self.c.Tr.FindBaseCommitForFixupTooltip + } + return []*types.Binding{ { Keys: opts.GetKeys(opts.Config.Universal.TogglePanel), @@ -112,6 +132,28 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty Description: openPullRequestDescription, Tooltip: openPullRequestTooltip, }, + { + Keys: opts.GetKeys(opts.Config.Files.CommitChanges), + Handler: self.workingTreeAction(self.c.Helpers().WorkingTree.HandleCommitPress), + Description: commitDescription, + Tooltip: commitTooltip, + }, + { + Keys: opts.GetKeys(opts.Config.Files.CommitChangesWithoutHook), + Handler: self.workingTreeAction(self.c.Helpers().WorkingTree.HandleWIPCommitPress), + Description: commitWithoutHookDescription, + }, + { + Keys: opts.GetKeys(opts.Config.Files.CommitChangesWithEditor), + Handler: self.workingTreeAction(self.c.Helpers().WorkingTree.HandleCommitEditorPress), + Description: commitWithEditorDescription, + }, + { + Keys: opts.GetKeys(opts.Config.Files.FindBaseCommitForFixup), + Handler: self.workingTreeAction(self.c.Helpers().FixupHelper.HandleFindBaseCommitForFixupPress), + Description: findBaseCommitDescription, + Tooltip: findBaseCommitTooltip, + }, { Keys: opts.GetKeys(opts.Config.Main.ToggleSelectHunk), Handler: self.toggleSelectHunk, @@ -383,6 +425,37 @@ func (self *MainViewController) focusedMainViewActions() types.FocusedMainViewAc return sidePanelContext.GetFocusedMainViewActions() } +// diffMainViewType reports what the side panel beneath the focused main view makes its +// primary action mean — staging (the working tree), patch-building, or nothing — or +// DiffMainViewTypeNone when this pane isn't focused or has no diff panel beneath it. The +// IsInStack guard is essential: NextInStack panics for a context that isn't in the stack, +// and GetKeybindings (which calls this) runs for off-stack panes too — at startup and +// during cheatsheet generation, where the stack is empty. +func (self *MainViewController) diffMainViewType() types.DiffMainViewType { + if !self.c.Context().IsInStack(self.context) { + return types.DiffMainViewTypeNone + } + if diffContext, ok := self.c.Context().NextInStack(self.context).(types.DiffMainViewContext); ok { + return diffContext.GetDiffMainViewType() + } + return types.DiffMainViewTypeNone +} + +// workingTreeAction wraps a working-tree command (commit, find-fixup-base) so it runs +// only while the focused main view shows the working-tree diff — the role the staging +// panel used to play, and the only place these commands mean anything. Over any other +// diff (a commit, a stash) the key is a no-op, so committing can't be triggered by +// accident while browsing history. Re-checks on each press rather than capturing the +// state at keybinding-registration time. +func (self *MainViewController) workingTreeAction(action func() error) func() error { + return func() error { + if self.diffMainViewType() != types.DiffMainViewTypeStaging { + return nil + } + return action() + } +} + // primaryAction acts on the selected diff line(s) — a single line, a range, or a hunk — // delegating to the side panel beneath the focused main view, since what the action means // is the panel's business: the working tree stages, while commits toggle the selection diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 82ef1ce9d..f965d6e9f 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -441,6 +441,7 @@ type IContextMgr interface { CurrentSide() Context CurrentPopup() []Context NextInStack(context Context) Context + IsInStack(c Context) bool IsCurrent(c Context) bool IsCurrentOrParent(c Context) bool ForEach(func(Context))