1029 Commits
Author SHA1 Message Date
Stefan HallerandClaude Opus 5.5 40a1027b89 Draw the commit graph in the demos with the detailed style
The 'auto' style doesn't recognize the terminal that vhs records, so it
would draw the classic graph. Now that the recordings have a font for
the branch drawing symbols, use the detailed graph in all demos.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 18:29:35 +02:00
Stefan HallerandClaude Opus 5 212142ff74 Confirm the nuke prompt in the nuke_working_tree demo
The recording of this demo stops with the confirmation popup still on
screen. The explosion animation and the emptied Files panel that the demo
exists to show never happen at all. Nuking the working tree has asked for
confirmation since 238fdd573c, and the demo only selects the menu item, so
the nuke never runs. The demo asserted nothing about the outcome, so it
stayed green all the while.

Answer the confirmation, and assert that the Files panel ends up empty.
That assertion also holds the recording open until the animation has
played out and the panel has been refreshed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 18:04:50 +02:00
Stefan Haller 115e3fb389 Update demos and readme for new staging-in-main-view functionality
We don't bother rendering the updated or new demos yet, because we are
about to change them to a different format.
2026-10-05 17:13:31 +02:00
Stefan HallerandClaude Opus 5 f3e8d88019 Run demos at the size they are recorded at
How a demo behaves depends on how much of a list fits on the screen, and
`just e2e` ran demos on a 150x100 screen while we record them at 120x35.
So a demo could pass the test suite and still fail partway through a
recording, and there was no way to find out short of recording it.

Give a demo the recording size when it doesn't ask for a size of its
own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 17:13:31 +02:00
Stefan HallerandClaude Opus 5 a2dd3327eb Check the line we start on when navigating to a list item
NavigateToLine looks for its target among the lines the view has
rendered. When a list is scrolled, the view holds only the part of it
that is on screen, so the target may not be among them. For that case
the helper jumps to the top of the list and walks down instead.

That walk presses a key before it looks, so it never sees the item it
starts on, which after jumping is the first item of the list. A target
sitting there is reported as missing. The shorter the terminal, the more
of a list is scrolled out of view and the more often that walk is
needed, so this surfaced once the demos began running in a 35 line
terminal.

Check the line the walk starts on before moving off it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 17:13:31 +02:00
Stefan Haller dd0029dd35 Use accordion mode in diff_commits demo
When running the demos at a hight of 35, as the new recording mechanism
will, this demo failed because the commits list was too small to show
both commits at the same time, and NavigateToLine has a bug that
prevents it from finding it by going to the top and pressing down until
it matches. We are going to fix that bug in the next commit for similar
future situations, but we also solve the problem here by setting the
side panels to accordion mode so that more commits are visible; this
looks better for this demo anyway.
2026-10-05 17:13:31 +02:00
Stefan HallerandClaude Opus 5.5 1f05a6b152 Keep copied commits when switching to another worktree or repo
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>
2026-10-05 16:05:40 +02:00
Stefan HallerandClaude Opus 5.5 205fad57fa Keep an emptied pane empty
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>
2026-10-05 14:55:13 +02:00
Stefan HallerandClaude Opus 5.5 f8fa6ee1a4 Add a test for emptying a pane before its diff's task is created
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>
2026-10-05 14:55:13 +02:00
Stefan HallerandClaude Opus 5.5 5641e8186b Show what was asked for last in the main view
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>
2026-10-05 14:55:13 +02:00
Stefan HallerandClaude Opus 5.5 4d55e67e2e Add a test for switching tabs twice in rapid succession
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>
2026-10-05 14:55:13 +02:00
Stefan HallerandClaude Opus 5 07e18b4085 Link the file names in a diffstat to where each file's diff begins
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>
2026-10-05 12:44:17 +02:00
Stefan HallerandClaude Opus 5 e2eab7c956 Add a menu of the diff's files to jump to one directly
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>
2026-10-05 12:14:02 +02:00
Stefan HallerandClaude Opus 5 ef71bcf9b7 Open the selected diff line in the branch's pull request
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>
2026-10-05 12:05:39 +02:00
Stefan Haller 6990c06880 Open a clicked diff line in the editor
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.
2026-10-05 11:55:14 +02:00
Stefan HallerandGitHub Copilot c04c192659 Remove the last staging-panel language from tests
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandClaude Opus 5 0b47b0013a Let wrapLinesInDiffView govern the two main panes
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot aba33ad93b Name diff options after the view that now uses them
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 5d72d7da99 Allow context changes while building a custom patch
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 9ddd5fc65f Remove the staging and patch-building panel shells
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot a825c5f7a7 Open file diffs in the focused main view
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot c38560bfa2 Move the last explorer workflows onto the focused diff
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 0902d0a5ea Drop test steps owned by the retiring explorers
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>
2026-10-05 11:44:42 +02:00
Stefan Haller f1232bbfd9 Migrate range select tests to focused main view 2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 59df93bfd5 Exercise commit and stash flows through the focused diff
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 4c0358d27f Build mixed custom patches from the focused diff
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot a6b5dcdfdb Keep partial rename patches on the focused diff
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>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot f3e98ed222 Manage patch lifetime from the focused diff
Removing a whole or partial patch from its source commit, and replacing
a patch when the user selects another commit, must not depend on
entering a patch-building panel. Keep those state transitions covered
through the diff that now owns patch selection.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 8bfd86ca2c Build applied patches from the focused diff
Applying and reverse-applying custom patches must keep their staging,
dirty-worktree, and conflict behavior after the patch-building panel is
removed. Select the source lines in commit diffs while retaining every
assertion about the resulting working tree and conflict resolution.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot b45df1b612 Keep historical diff actions off the explorers
Editing a line whose working-tree position shifted, discarding part of
an added file, and giving up a patch by escaping are all behavior worth
retaining. Drive them through the focused commit diff so their coverage
no longer depends on the patch-building panel.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 41fd1d1c86 Move partial-patch destination tests onto the commit diff
Line-level patch moves must keep their behavior for added and deleted
files, adjacent additions, stacked branches, and conflicting earlier
destinations. Select those lines directly in the focused diff so only
the intended changes move and file-level metadata stays with the source
when appropriate.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 78d0898911 Move whole-patch destination tests onto the commit diff
Moving a complete patch must preserve each file operation whether its
destination is a new commit before or after the source, or an existing
commit earlier or later in history. Build those patches as ranges over
the focused diff so the coverage survives removal of directory toggling
in the patch builder.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 3c36c066ed Keep patch-move conflict coverage on the focused diff
Conflict and dirty-worktree handling happen after a custom patch is
built, but their tests must no longer rely on either explorer to build
it. Select whole and partial patches in the commit diff while preserving
the rebase-conflict and stash-restoration assertions.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 5ab45d93d1 Move index-operation edge cases onto the focused diff
Patch movement still needs coverage for partial modifications, adjacent
additions, custom diff settings, and selections spanning files after the
patch-building panel goes away. Drive each through diff-line identities
while keeping the resulting commit and index assertions unchanged.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot b75a5930a9 Preserve whole-file operations for added and deleted files
A focused diff must replace the patch builder's whole-file toggle
without losing file creation or deletion metadata. Ordinary modifications
and renames must remain line-level selections even when every changed
content line is selected.

Ask the patch builder's canonical raw diff whether it consists of one
hunk containing only additions or only deletions and no context. Use the
whole-file operation only when the selection also covers every change.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 9f6270692a Expose the missing whole-file semantics in the focused diff
The patch-building panel could move an added file as a file, while
selecting every visible change through the focused diff currently moves
only its content and leaves the empty file behind. Keep the intended
index state beside the current one so the replacement flow demonstrates
that gap before it is fixed.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot abf24eaa74 Exercise patch-move selection recovery through the diff
The commit rewrite triggered by moving a custom patch must keep the
user's place without relying on the patch-building panel. Build the
patch and leave the affected range selected in the focused diff so the
regression test follows the UI that survives this branch.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot aeaaf3df57 Keep whole-file discard coverage on the surviving diff view
Removing the staging panel must not lose the handoff that happens when a
file's final change disappears. Drive that behavior through the focused
main view so the test continues to require focus and selection to follow
the files panel onto the next diff.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandGitHub Copilot 2be96483fa Let focused diff tests own shared staging behavior
Keeping the same staging interactions covered through the explorer would
pin the suite to the panel that this branch removes. The focused main
view now owns line, range, hunk, navigation, search, context-size, and
rapid-input coverage.

Co-Authored-By: GitHub Copilot <copilot@github.com>
2026-10-05 11:44:42 +02:00
Stefan HallerandClaude Opus 5 1e58904a93 Take lines back out of the custom patch from the pane showing it
Space in the pane previewing the patch had nothing to act on: everything
there is in the patch already, so it can only mean taking those lines back
out, in the way that space in the staged side of the working tree's diff
only unstages.

A line of the patch can't be found by its number: the patch renumbers
whatever follows a change it leaves out, so a line of it names a line of
the commit's diff that isn't the one shown. A line is identified instead
by its position among its file's changes, counted in the diff the patch is
shown as. That is the same position it has among the changes the patch
holds for the file, since both are in the order the file has them. This
holds however the rendering came out, so a renderer that groups a hunk's
deletions before its additions, or leaves a change out altogether, no
longer matters.

The pane's rows are stated in terms of the trees the patch was
materialized into, so the paths come back to the repo's own before
anything is looked up in them. This also lets the lines there be copied
and edited, as the lines of a commit's diff can be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:33:11 +02:00
Stefan HallerandClaude Opus 5 b6ce478d46 Materialize the custom patch so the diff renderer can show it
The pane beside a commit's diff showed the patch being built from it by
assembling the text itself. That text could not be handed to a diff
renderer the way a diff can: a stdin filter might have coped, but a tool
that diffs two files could not. Its idea of how much context to show
around a hunk was also its own rather than git's.

Materialize the patch instead: write each of its files as it is before the
patch into one tree and as it is after into another, and let git diff the
two trees. The patch becomes a diff of real files, rendered by whatever
renders the rest of them, with git's own context around it. Its lines can
then be pointed at; taking them back out of the patch will need that.

The trees are named a and b, so that with git's own prefixes suppressed
the paths read like an ordinary diff's over the repo's own paths. They are
written when the patch changes rather than when it is shown, the patch
builder counting its own versions for that, and they go away with the
patch.

A renamed file is materialized under the name the patch expects to find it
under. Where the patch carries the rename, that is the name the file had
before, so the rename comes out as a rename. git names the trees
themselves in the two rename lines, having only the two paths to go by.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:33:11 +02:00
Stefan HallerandClaude Opus 5 770f788ef8 Mark the lines of a commit's diff that are in the custom patch
A patch built from what is on screen has to show what is in it, over the
diff those lines were taken from — which may be a diff renderer's
rendering of it, whose bytes are none of ours to touch.

So the marks are drawn in a gutter over the content, from the lines the
patch holds rather than from where they were drawn last. They are worked
out again whenever a pane's content settles and whenever the patch
itself changes. Working them out as the content settles keeps them right
across a renderer switch, a change of context size, or a walk through
the commits.

They are shown whenever the main view shows the diff the patch is built
from, whether or not it has the focus. The pane beside the diff previews
the patch all the while, and marks that came and went with the focus
would look like a bug while browsing. A diff of any other commit gets
none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 11:33:11 +02:00
Stefan HallerandClaude Opus 5 4d23c06eb7 Build a custom patch from a commit's diff, and discard lines from it
The five panels that show a commit's diff now offer on it what the patch
explorer offers on the one file it can show at a time: space takes the
selected lines into the custom patch being built, or back out of it again,
and d takes them out of the commit itself.

So a patch can be built straight from the diff already on screen, over
several files at once and without entering the commit's files first — and
from a stash entry or a reflog entry too, whose diffs the explorer could
reach but which had no patch preview of their own to show for it.

The two arrive together because they are one answer to what a commit's diff
offers, and because d needs exactly what space builds: a patch of the
selection, which the rebase then removes from the commit. Which of the two
space is doing is decided by the first selected line, as toggling a file in
the commit files panel is, and the selection then moves past the lines
dealt with — they are still in the diff, a patch leaving the commit alone,
so a second press would otherwise take them straight back out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:33:11 +02:00
Stefan HallerandClaude Opus 5 b5dd073056 Keep the diff selection when a commit is rewritten under it
Removing lines from a commit, moving a custom patch out of one, undoing
either — none of it goes through the focused main view, so a selection over
that commit's diff was left where it was, painted over a rendering of a diff
that no longer exists.

Every render now asks whether it is showing a different diff than the one on
screen, which is what a rewritten commit looks like, and puts the selection
back on the change that has taken its place — the same reveal an action in
the view does for itself. It stands down for a render of the same diff,
where a selection, perhaps a range still being made, is exactly right as it
is, and for one that something more precise is already waiting to place.

The key a render is remembered under moves ahead of anything that might
change the command's arguments, so that it says which diff is being
rendered and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:33:11 +02:00
Stefan HallerandClaude Opus 5 11671fd86e Edit the selected hunk from the focused main view
The staging view can hand the hunk you are on to an editor and apply what
comes back, which is how you stage something the diff cannot express: half
of a changed line, or a change written differently from either side. The
focused main view has to be able to do the same before that view can go.

The hunk is git's own, context and all, rather than lazygit's block of
adjacent changes: an editable patch is one that still applies, and the
context lines are what let git place it. What the editor leaves behind is
applied whole rather than matched against the file's diff again, the point
being that it says something the diff didn't.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:20:31 +02:00
Stefan HallerandClaude Opus 5 603e254ec4 Hold input back until the selection has moved on
Two space presses in quick succession only staged one hunk. Input is
withheld until the refresh has landed, which is enough where the diff is
rebuilt on the spot, but the main view re-renders asynchronously: the
selection only moves to the next change once that render is on screen, so
the second press acted on lines that were no longer in the diff, and
staged nothing.

So the wait is now for the selection to be where the work carries on from,
rather than for the model. A restore therefore has to say when it is done —
which it can be either way, since a view given a message rather than a
re-render now gives up the restore it was holding instead of leaving it to
claim some later render.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:20:31 +02:00
Stefan HallerandClaude Opus 5 308e696fc7 Cover the cases where the next change isn't the obvious one
Three cases where landing on the change that took the place of the one acted
on is not the same as landing on the next line, or on the same line number:
staging an inserted line moves every later line of the file, so the hunk
below it is somewhere else afterwards; consecutive deletions all sit at the
same place in the new file, so nothing but their order tells them apart; and
unstaging half of a modification carries on in the pane the staged side has,
which is where the work was already.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:20:31 +02:00
Stefan HallerandClaude Opus 5 b967d02616 Show a selection only over the diff the panel offers
Some merge conflicts can only be resolved by picking a side. The files
panel explains those rather than diffing them, and where one side deleted
the file and the other modified it, git's diff of that modification is
shown below the explanation. The focused main view took those change lines
for a diff of its own. It drew a selection over them and offered to stage
hunks of a file whose conflict staging can't resolve.

So have a render say whether it holds the diff the panel offers in the
main view, and put a selection only on one that does. Every diff render
already goes through NewMainViewDiffTask, so it says so for itself; the
custom patch preview, assembled as text rather than run as a command, says
so through NewMainViewDiffStringTask. Establishing a selection asks the
pane the same question rather than looking for change lines itself.

The selection has been wrong over this content since "Show a selection in
the focused main view" introduced it, and the fix belongs there. It lands
here instead because a render had no way to say what it holds until "Show
git's own diff when the renderer's can't be acted on" gave every diff
render one constructor to go through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:20:31 +02:00
Stefan HallerandClaude Opus 5 e85c0cb419 Show git's own diff when the renderer's can't be acted on
A diff renderer may lay a diff out however it likes: line numbers in a
gutter, the +/- column replaced by colour, the two sides in columns. Once
it has, we can only tell which line of which file a row shows if the
renderer says so. Under a renderer that doesn't say, the main view holds
a diff that can be read but not staged, edited or copied from. That is no
good now that the main view is where you stage.

So focusing it brings git's own diff instead, and every re-render while it
stays focused keeps to that, so staging a hunk doesn't flip back. Browsing
is untouched: you see what the renderer produced until you focus the view
to act on it.

Whether the renderer says anything is settled by asking it rather than by
watching it work: run it on empty input and see whether it announces the
protocol. Announcing is a property of the renderer, so the answer is known
before we render anything, and a diff with no lines to describe can't fool
it. Watching would have to see a diff go by first, and a binary file's diff
holds nothing that would tell the two cases apart. The answer is remembered
until the renderer changes. git itself is asked the same question, with the
renderer's own arguments, since it announces itself for exactly the formats
whose output can't be read back as a diff.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:20:31 +02:00
Stefan HallerandClaude Opus 5 ae538a8583 Commit and find a fixup base from the focused main view
Staging in the main view leaves you looking at the working tree's diff with
everything you meant to stage in the index — and, until now, having to go
back to the files panel to commit it. The commit keys, and the one that
finds the commit a fixup belongs to, are offered there too, so that the
whole staging-to-committing round happens in one place.

They act on the working tree, which the main view only sometimes shows, so
they do nothing over a commit's or a stash's diff — browsing history can't
commit by accident — and are listed only where they do something. The check
happens per press: what the main view is showing changes as the user moves
about, while the bindings are registered once.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:20:31 +02:00