Commit Graph
8552 Commits
Author SHA1 Message Date
Stefan Haller 2cff798d10 Update translations from Crowdin 2026-10-05 18:44:25 +02:00
Stefan Haller a214743a1e Change the demo gifs to videos and update them for the newly added features (#6073)
The demo videos were rendered as animated gifs, which is less than
ideal, because they start playing automatically, they loop but users
can't tell when they start over from the beginning, and they can't be
paused or rewound.

Convert them to videos instead so that users see a playhead and the
usual player controls. Leave a bit of room at the bottom for those
controls; different browsers render those differently, so it's a bit of
a best effort solution.

Also take this opportunity to update some of the demos for the changed
functionality of #6035, #6039 and others.
2026-10-05 18:42:30 +02:00
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.5 ff8d74971f Add a font with the branch drawing symbols for the demo recordings
The terminal that vhs records is xterm.js running in a browser, and the
xterm.js that ttyd bundles doesn't draw the branch drawing symbols
itself. So they have to come from a font, and SauceCodePro NF doesn't
have them, nor does any font that macOS comes with. The detailed commit
graph shows up as garbage.

Newer versions of xterm.js do draw the symbols, but only in the WebGL
renderer. vhs forces the canvas renderer, and xterm.js has removed that
since, so moving to a newer xterm.js would mean patching both ttyd and
vhs.

The Flog Symbols font has the symbols, but it draws its lines for a
smaller cell than the one xterm.js uses at our font settings. As a
fallback font, it leaves a gap at the edges of every cell, and each line
of the graph comes out dashed.

Add a copy of the font that fits the cells of the recordings. xterm.js
clips each character to its row, so the vertical strokes reach well
past the top and bottom of the cell and end exactly at the row's edges.
It doesn't clip a character to its cell horizontally, so the horizontal
strokes reach only a little way into the neighbouring cells. Any
further, and they would show past the start of a bend in the next cell.

Give the font a bold face with the same outlines. lazygit draws the
lines of the selected commit in bold, and without a bold face the
browser makes the symbols bold itself by thickening them. That leaves
gaps where they meet.

Add the script that made the font, and list the font after
SauceCodePro NF in demo/settings.tape so that the browser takes the
symbols from it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 18:29:35 +02:00
Stefan HallerandClaude Opus 5 5a56c609c2 Add a script for re-recording every demo a page embeds
Changing demo/settings.tape, or anything about how lazygit looks, dates
every recording at once, and re-recording them one at a time means
running the recorder fifteen times and pasting fifteen new URLs.
Attachment URLs say nothing about where they came from, so there is also
nothing to tell you which demo a video in the README is of.

Name the demo in a comment above each video. GitHub drops the comment
when it renders the page, so it costs the reader nothing, and it gives
us a way back from a page to the demo that produced it. Then walk those
comments, re-record each demo and rewrite the URL below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 18:29:35 +02:00
Stefan Haller 9c86ef15b2 Change the demos in the README to videos 2026-10-05 18:29:35 +02:00
Stefan HallerandClaude Opus 5 d24bb08384 Record demos with vhs and publish them to GitHub's attachment store
The demo gifs in the README start on their own, loop without telling the
reader where a run begins, and give no way to pause, seek or replay. A
video with the browser's own controls fixes all three, but GitHub plays
a video in a README only when it is served from its own attachment
store. If you commit one to the assets branch and link it the way we
link the images, GitHub drops the whole <video> element when it renders
the page.

Replace terminalizer with vhs. vhs records the demo straight to mp4
rather than going through a gif, and it takes the terminal size, font
and colours from demo/settings.tape. Then upload the result from the
endpoint that GitHub's own drag-and-drop upload posts to, and print the
tag to paste into the page.

Uploading alone is not enough. An attachment is readable only by people
who are signed in to GitHub until a posted comment in the repository
refers to it, and a README on a branch does not count. So post each
recording to a collecting issue and wait until the video can be fetched
without a token. Miss that step and the video plays for whoever recorded
it and 404s for every other reader.

Two things about the frame. Pad the bottom, because the browser draws
its playback controls over the video and they are tall enough to cover
the line where the demos put their captions. And cut the end: vhs
records until the marker reaches the screen, by which time lazygit has
exited and the shell has painted its prompt back over the demo. A
browser holds the last frame of a video once it has played to the end,
so leaving those frames in would end every demo on a terminal prompt
and leave it there.

Pass --no-upload while you are still working on how a demo looks. That
writes the video to demo/output and stops, so trying out a colour or a
font costs nothing but the recording itself.

The recording is sharper and smaller than the gif it replaces, at
1866x1230 and 25 fps against 1140x828 and about 5 fps. It also costs the
reader nothing until they press play, whereas the gifs are fetched every
time the README is opened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 18:04:50 +02:00
Stefan HallerandClaude Opus 5 0026f853c7 Give the tests a visible inactive border colour
The integration test config asks for `black` inactive borders. In a
recording that comes out as a mid grey, because the recording theme
remaps the terminal's black to #7a7a7a. In an ordinary terminal it is
real black, so when you watch a test with `just e2e-cli` the inactive
frames all but disappear against the background.

Name the grey directly instead (but a little bit brighter than the #7a
we had before), so that the frames look the same either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 18:04:50 +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 Haller 257b3757d3 Fix the rule below a pull request's header being rendered with the wrong width (#6091)
When selecting a branch that has a pull request associated with it, the
main view shows a PR header and a rule below it that separates the
header from the branch log. That rule didn't always have the correct
width: one scenario where it didn't was when you look at a diff with
both staged and unstaged changes whose view is split side-by-side (e.g.
because you use `gui.mainPanelSplitMode: horizontal`), and then jumping
to the branches panel with a PR branch selected. In that case, the rule
was still rendered to the shorter width of the split view.
2026-10-05 17:12:27 +02:00
Stefan HallerandClaude Opus 5.5 576017c00b Lay out the rule below a pull request's header to the main view's width
The rule was as wide as the main view was when the render was asked
for. If the layout then changed the width of the main view, the rule
came out too long or too short.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:38:13 +02:00
Stefan HallerandClaude Opus 5.5 5235083bd2 Let the prefix of a render be laid out to the width it renders at
The rule below the header of a branch's pull request is drawn as wide as
the main view. Its width is read when the render is asked for, before
the layout has settled it. If the layout then changes the width of the
main view, for example because it unsplits the view for the content
that comes next, the rule comes out as wide as the view used to be.

Make a prefix a function that is called with the width the render is
laid out to, once the layout has run. The function it returns produces
the text on the render's own goroutine before the command starts. This
way a prefix that takes a while to produce, such as one that runs
commands of its own, holds up only the render and not the UI.

All prefixes are still static here; the next commit lays out the rule
of the pull request header to the width.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:38:13 +02:00
Stefan HallerandClaude Opus 5.5 2b57733541 Hoist the check for a stopped task to the top of NewCmdTask
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>
2026-10-05 16:38:13 +02:00
Stefan Haller a5497bacf4 Keep copied commits when switching to another worktree or repo (#6090)
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.
2026-10-05 16:26:20 +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 Haller fc85e9353c Fix wrong main view content in rare edge case situations (#6089)
See the individual commit messages for what exactly is fixed here.
2026-10-05 15:54:12 +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 edf30f6455 Tie the loading state of a view to the task asked for last
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>
2026-10-05 14:55:13 +02:00
Stefan HallerandClaude Opus 5.5 17d1db2e6c Add tests for when a view counts as loading
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>
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.5 4ac61d6c21 Set the caption of an integration test on the UI thread
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>
2026-10-05 13:55:37 +02:00
Stefan Haller b8a7dc7d3c Link the file names in a diffstat to where each file's diff begins (#6044)
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.
2026-10-05 12:49:35 +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 Haller 7f5d20a29f Jump to a file of the diff from a menu (#6043)
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.
2026-10-05 12:44:13 +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 0e0236ad7b Move where a jump lands into the diff line helper
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>
2026-10-05 12:14:02 +02:00
Stefan HallerandClaude Opus 5 4fe993442c Ask a diff where each of its files begins
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>
2026-10-05 12:14:02 +02:00
Stefan HallerandClaude Opus 5 89b2697936 Name the file a diff row belongs to in the repo's terms
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>
2026-10-05 12:14:02 +02:00
Stefan Haller 81abf5a839 Open the selected diff line in the branch's pull request (#6042)
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.
2026-10-05 12:13:58 +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 HallerandClaude Opus 5 d8cb217bad Ask for the panel beneath the focused main view in one place
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>
2026-10-05 12:05:39 +02:00
Stefan HallerandClaude Opus 5 2c5629aac9 Name a diff line's file in the repo's terms in one place
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>
2026-10-05 12:05:39 +02:00
Stefan HallerandClaude Opus 5 1228076501 Ask one place whether a branch has a pull request
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>
2026-10-05 12:05:39 +02:00
Stefan Haller 1d99e2e81e Alt- or shift-click a diff line to open it in your editor (#6041)
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.
2026-10-05 12:05:36 +02:00
Stefan Haller 150dfadf9b Acknowledge editor clicks in the diff
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.
2026-10-05 11:55:14 +02:00
Stefan Haller 7aff6d7eae Give views a transient line flash
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.
2026-10-05 11:55:14 +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 Haller 9fdefc418d Keep mouse gesture modifiers stable
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.
2026-10-05 11:55:14 +02:00
Stefan Haller 345191a16f Share diff-line editing with mouse actions
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.
2026-10-05 11:55:14 +02:00
Stefan Haller ef5f0dccfb Let mouse bindings work behind focused popups
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.
2026-10-05 11:55:14 +02:00
Stefan Haller 23c0b40fce Separate redraws from view-line invalidation
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.
2026-10-05 11:55:14 +02:00
Stefan Haller dc6ea4d515 Remove unused per-line highlighting
View.SetHighlight has no production callers.
2026-10-05 11:55:14 +02:00
Stefan HallerandClaude Opus 5 15532b74b9 Cleanup: remove error return value from Gui.SetRune, Gui.draw() et al
These always returned nil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 11:55:14 +02:00
Stefan Haller 2aac999354 Remove the staging and patch-building panels (#6040)
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.
2026-10-05 11:55:10 +02:00