The next commit checks whether the task was stopped once more before
the command starts. Use the same check there as in the loop that reads
the command's output.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If you copy commits with shift-C and then switch to another worktree,
the copied commits are gone, so you can't paste them there with shift-V.
This is a common workflow: copy commits from the branch that is checked
out in one worktree, maybe drop them there right away, then switch to
the worktree of another branch and paste them.
Create the cherry-picking mode once and share it between all repo
states. This also lets you paste in a different clone of the same repo,
as long as that clone has the copied commits (for example because you
fetched them there). If the current repo doesn't have them, git
cherry-pick fails with "bad object", and we show this as an error.
If you copy commits with shift-C and then switch to another worktree,
the copied commits are gone, so you can't paste them there with
shift-V. This is a common workflow: copy commits from the branch that
is checked out in one worktree, maybe drop them there right away, then
switch to the worktree of another branch and paste them.
The copied commits live in the cherry-picking mode, and each repo state
creates its own. We keep a separate repo state per worktree, so every
worktree starts with an empty clipboard.
Create the cherry-picking mode once and share it between all repo
states. This also lets you paste in a different clone of the same repo,
as long as that clone has the copied commits (for example because you
fetched them there). If the current repo doesn't have them, git
cherry-pick fails with "bad object", and we show this as an error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Emptying a pane of the main view clears it right away, but leaves the
view's tasks alone. If a diff was asked for before, and its task is
only created after the layout, that task fills the pane again. A diff's
task that is still reading can also write into the emptied view.
Give the pane an empty render as well. The render takes its place among
the view's tasks, so the earlier diff's task isn't created, and it stops
a task that is still reading. Once that task has stopped, the render
empties the view again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If the lower pane of the main view is asked to show a diff and is then
emptied before the next layout, it ends up holding the diff. Selecting
a file with staged changes and moving back to one without any in rapid
succession does this. The pane is hidden then, but when it is shown
again, it shows the other file's diff until its next render replaces
it.
Emptying a pane clears the view right away, but leaves the view's tasks
alone. So the diff's task, which is only created after the layout,
fills the pane again. A task that is still reading a diff into the pane
isn't stopped either, so it can write into the emptied view.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The loading state is set when a command task is asked for, and cleared
when a command task reaches the end of its input, whichever task that
is. A task that is stopped before then doesn't clear it, so a message
that replaces the task leaves the view loading. And a task that was
asked for earlier clears the state that a later one has set.
Count the view as loading only while the task asked for last is a
command task that is still reading its input. When a task reaches the
end of its input, let it end the loading only if it is that task.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If a message replaces a command's output while the command is still
being read, the view keeps counting as loading until another command
has been read to the end. Until then, the layout doesn't clamp the
view's scroll position to its content, and IsSingleHunkForWholeFile
returns false.
If an earlier command reaches the end of its input after a later one
has been asked for, the view stops counting as loading, although the
later command hasn't started yet. The layout can then clamp the scroll
position to the earlier command's output before the later command has
been read.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If a diff and then a message are asked for in the main view before the
next layout, the main view shows the diff. One way this happens is
switching tabs twice in rapid succession. Another is discarding the
changes of the only changed file. Closing the menu asks for the file's
diff, and if the files are refreshed before the layout, "No changed
files" is asked for next. The main view then stays empty, because by
the time the diff runs, the file has no changes. This made the
hide_selection_when_changes_vanish test fail now and then.
A diff's task is only created after the layout, since the layout settles
the width that the diff is laid out to. A diff renderer's task has been
created there since 8b8343b8a9, and the task for git's own diff since
0afb94e97b. A view shows the task that was created last, so the diff's
task replaced the message's.
Reserve the diff's place among the view's tasks when the diff is asked
for, and give its task that place when it is created after the layout.
If another task has been asked for in the meantime, don't create the
diff's task at all.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If two tab switches are handled before the next layout, and the first
tab shows a diff in the main view while the second one shows a message,
the main view ends up showing the diff.
A diff's task is only created after the layout, since the layout
settles the width that the diff is laid out to. A message's task is
created right away. So the diff's task is created after the message's,
and replaces it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An integration test sets its caption in the options view from the
test's own goroutine, while the UI thread may be drawing that view. The
race detector reports this now and then, and fails whichever test
happens to be running.
Set the caption on the UI thread instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
13th PR in the stack of PRs towards staging hunks directly from the main
view. This one is stacked on #6043, and it concludes the work.
When selecting a commit, the main view shows its diff with the message
first, then a diffstat listing all files changed by this commit, and
then the actual diff body. The diffstat reads like a table of contents,
and it has always bothered me that it isn't interactive; I always wished
I could click on one of the file names to jump to that file in the diff.
Now you can. This PR turns the diffstat entries into clickable links
that get underlined as you hover over them, like other hyperlinks in
lazygit do. Clicking one scrolls the diff to the beginning of the
clicked file, like with the `<ctrl-g>` menu that was added in the
previous PR. If you do this while the commits panel is focused, the
focus stays there.
A diff opens with a diffstat naming every file in it, right above the
diff of each of them. Selecting a commit puts that list in front of you,
naming the same files the menu of the diff's files offers, and the menu
is still the only way to any of them.
Make each of those names a link that goes to where that file's diff
begins, as picking it from the menu does. The panel keeps the focus, so
a file of the commit being read is a click away and the selection stays
on the commit.
The names are found in the output as it is written to the pane, for next
to nothing. The diffstat is git's own text whichever renderer the diff
goes through — delta, diff-so-fancy and difftastic all pass it on
untouched — and it comes first, so the scan for it ends with it and
nothing below is looked at.
The link states the name as the diffstat does, and which file that names
is worked out on the click, against the files the diff turned out to
hold. That is the point at which a name the diffstat cut off behind
"..." or compacted to the "{old => new}" form of a rename can be
recognized at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12th PR in the stack of PRs towards staging hunks directly from the main
view. This one is stacked on #6042.
`<ctrl-g>` when a diff is showing (focused or not) opens a menu of the
files it contains, in the order the diff shows them and by the paths the
repo knows them by. Picking one scrolls that file to the top of the
view. The menu filters as you type, which makes it easy to quickly jump
to a file in a huge commit touching many.
A diff with only one file in it says so in a toast rather than opening a
menu with nothing to choose from.
n and N step through the files of a multi-file diff one at a time, which
is a long way to the far end of a commit touching a hundred files.
Bind ctrl+g to a menu of the diff's files, in the order the diff shows
them and by the paths the repo knows them by; picking one goes to where
its diff begins, exactly where stepping to it with n would have left
you. The menu filters as you type, so the file you have in mind is a few
characters away however many the commit touches.
The key works in the panel the diff belongs to as well as in the diff
itself. A commit is read from the commits panel, so being able to jump
from there saves focusing the diff and leaving it again for the next
commit; the diff scrolls to the file and the focus stays in the panel.
It is ctrl+g rather than f because a key that works in every panel has
to be free in all of them, and f is fetch in the files panel and fixup
in the commits panel.
The command applies only while the main view is showing a diff, and is
left out of the keybindings menu where it isn't: over a branch's commit
log, or while a conflicted file has given the main section over to the
merge conflicts view.
The diff is read to the end before the menu is built, as searching it
does: a file below the part that has been read is in neither the list
nor the view. Each item names its file rather than the row that file
begins at, so that a diff re-rendered while the menu is up is jumped
into at the row the file begins at now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Where a focus or a click puts the focused main view's selection is
worked out in the diff line helper; where a jump puts it is worked out
on the main view controller, though it is the same question about the
same pane. A jump asked for from anywhere else — from a panel below the
pane, or from a click on a link in the diff — has no way to reach that
answer.
Move it over. The controller keeps placeNavigationTarget as the short
way to say it for the jumps it makes itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
File navigation works out where the neighbouring file begins by walking
the rows itself, forwards or, more laboriously, backwards. A menu of the
diff's files needs the same rows, all of them at once.
Extract fileStarts, which answers that for the whole diff, and have
navigation pick its neighbour out of the answer. The two then agree on
where a file begins by construction, and the walk backwards over a file
goes away.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The identity of a row is reported in the repo's terms already: the pane
previewing the custom patch shows a diff of the two trees the patch was
materialized into, and the paths of those trees are mapped back to the
repo's files before anything sees them. Asking which file a row belongs
to, which file navigation does, was the one query that skipped that step
and answered with the tree's path.
Pull the mapping out of inRepoTerms so that it can be applied to a bare
path, and put filePaths through it. A menu listing the files of the diff
will want to show them by name; where a renderer states the path of each
side of a change, this also has both halves belong to one file rather
than to the two trees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11th PR in the stack of PRs towards staging hunks directly from the main
view. This one is stacked on #6041.
With a commit's diff focused, open the branch's pull request in your
browser at the line you have selected. GitHub only, the anchor format
being GitHub's.
Which branch's pull request that is depends on where you are. The
commits panel answers with the checked-out branch, the sub-commits panel
with the ref you entered it from where that is a local branch, and the
commit files panel by asking the panel it was entered from. The files
panel, the stash and the reflog have no pull request to point at, and
the command isn't offered there at all.
The anchor format is `<pr url>/changes/<sha>#diff-<sha256 of the
path>R<line>`. It is derived by experiment; GitHub doesn't document it.
Reading a change in lazygit and saying something about it on GitHub
means finding the line again in the browser: open the pull request, find
the commit, find the file, scroll to the line. The line is already under
the cursor here.
Bind G in the focused main view, the key the commits panel opens the
pull request with, to open it at the line the selection is on. The URL
names the commits whose diff is on screen, so that the line numbers of
the diff are the ones the page shows, the file by the SHA-256 of its
repo-relative path, and the line by the side of the diff it is on: R for
the new version of the file, L for the old one, where a deleted line is.
GitHub documents none of that; the form was read off the URLs its own
pages carry.
One commit is named by its hash. A range of them is named by the commit
the range starts after and the commit it ends at, the form the chooser
above a pull request's files uses. The commit a range starts after is
the parent of its oldest commit; where the range starts where the pull
request itself does, that parent is none of the pull request's own
commits, and the keyword BASE stands for it.
Which branch's pull request that is depends on the panel beneath. The
commits panel lists the commits of the checked-out branch, the
sub-commits panel those of the branch drilled into, and the commit files
panel shows the files of a commit from either. In a stack of branches,
each with a pull request of its own, those lists include the commits of
the branches below, and each of those commits is in the pull request of
its own branch. So the command looks upwards from the commit for the
nearest head of a branch with a pull request, and takes the listed
branch if it finds none. Panels showing a diff that no pull request has
a view of don't answer, and the command isn't offered over their diffs
at all.
Neither is it offered over a diff that is not the commit's own, where
the line numbers on screen are not the ones the page shows: a diff
against another ref in diffing mode, and the custom patch, whose lines
sit at the numbers the patch gives them.
A pull request holds only the commits of its branch that are pushed, and
its pages say they can't find any other commit. So the command refuses
where a commit of the diff is not one of the pull request's. Amend a
commit in the middle of the branch, and the diffs of the commits below
it still open; the ones above it sit on hashes the remote doesn't have.
A commit from before the branch, in a main branch already, is refused
too, and so is a range of commits that reaches across the head of a
branch in a stack, since its commits are in two pull requests. Whether a
commit is pushed is known only for the upstream of the listed branch.
For a branch lower in a stack, that is right as long as the branches of
the stack are pushed together.
Only GitHub pull requests are known, since that is where the pull
request data comes from. The whole path can't be exercised headlessly:
no pull request reaches the model without a GitHub token, so the test
covers where the command is offered and the three reasons it refuses.
The URL is unit-tested instead, both the anchor of a line and the way
the commits are named, and so is the choice of a branch in a stack.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two questions the pane answers from the panel beneath it each reach for
it themselves, guard included. Opening a line in a pull request needs it
twice more, for the branch and for the commit.
Extract sidePanelBeneath, which is also where the guard against asking
for the panel beneath an off-stack pane now belongs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A diff line carries the absolute path of its file, and both panels
acting on such a line turn it into the repo-relative one git speaks
themselves. Opening a line in a pull request needs that path too, to
name the file to GitHub by it.
Extract repoRelativePath, and have both panels use it. The files panel
gains the check for a path outside the repo that the other one had; a
path it used to pass on matches no file of the working tree either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two panels offer a branch's pull request today, and each looks it up in
the model's map itself and builds the same disabled reason from the same
string. The focused main view is about to offer a line of the diff in
that pull request, which would make three.
Put the lookup and the disabled reason on the host helper, beside the
pull request URL it already builds, and have both panels ask it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
10th PR in the stack of PRs towards staging hunks directly from the main
view. This one is stacked on #6040.
Alt-click or shift-click a line of any diff and it opens in your editor
at that line. It doesn't move the focus or change the selection, and it
works while a popup has the focus, so you can read a diff behind a menu
and jump into the code. This is especially useful while writing a commit
message, when you want to copy a function name from the code that's
showing behind the commit message editor.
Both modifier clicks are bound (alt and shift), because no single one
works in Ghostty, iTerm2 and VS Code alike. Each of those swallows or
rewrites a different one, so you have to test which one works in your
terminal.
A 200 ms flash over the clicked row acknowledges the click. An editor
can take a moment to appear, and until it does you are otherwise left
wondering whether anything happened.
Non-suspending graphical editors can take a moment to reach the
foreground, leaving a modified click with no visible response. Briefly
reverse the clicked row's selection bar after launching the editor so
the registered action is apparent.
Arm the flash only for a resolved diff row, keep newer clicks safe from
stale timers, and rely on suspension to clear the transient state before
terminal editors take over.
Actions that hand control to another application need visible
acknowledgement without moving a view's cursor or replacing renderer
colors. Reverse the narrow selection bar independently of selection
state, and clear transient flashes whenever the terminal UI suspends.
Clickable renderer gutters are small and unavailable on some diff rows.
Make the whole row an editor target without changing focus or selection,
including while a popup is focused.
Use both Alt and Shift because terminal mouse protocols do not deliver
either modifier consistently across Ghostty, iTerm2, and VS Code.
Bindings match modifiers exactly, so a modified press must not turn into
an unmodified drag or release halfway through the gesture. Capture the
press-time modifiers once and carry them until the button is released.
This also makes unbound modified clicks no-ops instead of silently
invoking plain-click behavior.
The selected-line keybinding and a modified click need the same path
from a rendered diff row to the editor. Give that operation an explicit
view-line argument before adding the mouse gesture.
Mouse events on views behind a popup are normally swallowed before their
bindings can run. Add an explicit early-dispatch opt-in for actions that
should remain live there, matching the phase where hyperlink clicks
already run.
A view can need repainting even when its cached wrapping is still valid.
Track that state independently so content-only flushes do not overload
tainted, whose only job is to request a viewLines rebuild.
9th PR in the stack of PRs towards staging hunks directly from the main
view. This one is stacked on #6039.
Everything the staging and patch-building panels did now happens in the
main view, so this PR removes them.
`enter` on a file, in the files panel or the commit files panel, focuses
the main view at that file's diff. Double-clicking the file row does the
same. Everything else about the flow is unchanged; there is simply no
separate panel to be in.
**Config changes, both migrated automatically:**
- `gui.wrapLinesInStagingView` → `gui.wrapLinesInDiffView`
- `gui.useHunkModeInStagingView` → `gui.useHunkModeInDiffView`
Notable change: `wrapLinesInStagingView` used to affect only the staging
panel, but not the normal diff view. The new `wrapLinesInDiffView`
affects the main view always, whether focused or not; I assumed this is
probably the desired behavior, but I don't use the option myself, so I
can't tell for sure.
**One thing to know if you use custom commands:** `staging`,
`stagingSecondary` and `patchBuilding` are no longer context names. If a
custom command still names one in its `context` field, that is now
reported as a config error which you need to fix manually.
The context size can now be changed while a patch is being built. The
refusal existed because the old patch explorer kept track of which lines
are included in the patch by their line indices in the diff, which would
change when changing the context size. The new mechanism doesn't have
this problem, because it keeps track of patch lines by their line
identity, which doesn't change.
In the staging panel it used to copy the selected text verbatim, but in
the focused main view it copies the diff lines (that's also what the
toast says), and now that the staging panel is gone, we can change the
text to make it more specific.
Generic range assertions and migrated commit tests should describe the views
they still exercise. Drop stale explorer wording so failures and test intent
no longer point contributors at UI that does not exist.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Users no longer enter separate staging or patch-building panels. Describe file
entry, pane switching, staging, and custom patch construction where those
actions now happen, and remove the deleted package from the developer guide.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Working in a diff used to mean the staging view, and this option said
whether the long lines there were wrapped. The main view does that work
now, and the option has had nothing to govern since the staging view went
away. Both panes wrap whatever they are given.
Make the option their wrap setting instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hunk defaults and wrapping no longer belong to a staging panel. Rename both
public keys with automatic migration, and remove panel-era English strings so
config, UI, schema, and generated docs describe the surviving interface.
Co-Authored-By: GitHub Copilot <copilot@github.com>
The focused diff addresses patch lines by identity, so changing how much
context is rendered cannot invalidate an active patch. Remove the
explorer era refusal and prove that another line can still be toggled
correctly after the rerender.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Nothing routes to the explorer contexts after line actions and file entry
moved into the normal main pair. Delete their contexts, views, main-pair
wiring, discovery API, test drivers, and selection-state package so the
surviving diff view is the only implementation.
Their names go from the contexts a custom command may bind to as well, so
that a config still naming one is reported as the config error it now is
rather than taken as a context we simply failed to find.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Focused main views now own selection, staging, patch construction, copying,
and context-size rerenders. Remove the parallel controller/helper stack and
its refresh scopes, while retaining custom-patch reset as mode behavior.
Co-Authored-By: GitHub Copilot <copilot@github.com>
The next commit deletes the controller this lives in, while the helper
itself is still wanted: it is what makes a copied selection paste
straight into code. Move it first, so that the deletion is a deletion.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Enter and double-click should take users to the diff where line actions now
live, whether the file belongs to the working tree or a commit. Share the
existing focus, raw-fallback, and selection setup while retaining directory,
submodule, and conflict-specific behavior.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Historical file removal and the staging/custom-patch demos still entered
the old panels even though their line actions already exist in the main
diff. Preserve their prompts, timing, and downstream results while changing
only the interaction surface.
Co-Authored-By: GitHub Copilot <copilot@github.com>
The generic filter and range-selection suites already exercise their state
machines on surviving views, while focused-diff tests own range selection
and drag autoscroll. Remove only the repetitions that enter an explorer,
along with the staging-panel screen-mode test whose UI no longer survives.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Line staging remains part of several broader workflows: committing staged and
unstaged changes, bypassing hooks, and stashing only the staged part of a
file. Drive those selections through the main diff pair while preserving
their commit, focus, and exact stash-content assertions.
Co-Authored-By: GitHub Copilot <copilot@github.com>
The broad regression for combining a whole file, a hunk, a range, and
individual lines must survive the patch-building panel. Keep the side panel
for the explicit whole-file operation and drive every content selection
through the commit's focused diff.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Selecting a rename's content changes must remain distinct from selecting
the file operation itself. Build and remove that partial patch through
the focused commit diff, and keep asserting that the rename stays in its
source commit after the content change is taken out.
Co-Authored-By: GitHub Copilot <copilot@github.com>