Commit graph

8217 commits

Author SHA1 Message Date
Stefan Haller fc134d643f Session notes: patched the three pager emitters + the spec (§21.31) 2026-08-08 12:59:00 +02:00
Stefan Haller a6fedd0255 Spec: define the version-only handshake record
Add §4.4: a conforming pager emits a version-only OSC 1717 record (no further
fields) as its first output, so a host can probe it on an empty diff — which
emits no per-line records — and tell "speaks the protocol" apart from
"unsupported pager", content-independently. Used by the focused main view's
raw-diff fallback to decide whether a pager's output is actionable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 40c035fd79 Session notes: detection reworked observe → probe (§21.30) 2026-08-08 12:59:00 +02:00
Stefan Haller c0ee7de776 gocui: swallow the version-only diff-metadata handshake OSC
A metadata-aware pager emits a version-only OSC 1717 record (no fields) as its
first output, to announce it speaks the diff-line-metadata protocol so lazygit
can probe for it. It isn't per-line metadata, so on a real render it must be
swallowed whole rather than lingering in the accumulator and attaching to the
following diff-header line (or, if newline-terminated, producing a phantom blank
line). Drop any OSC 1717 payload with no fields at its terminator; per-line
payloads always have fields, so they're kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller f3ef02556a Session notes: (D) raw-diff fallback for unsupported pagers done (§21.29) 2026-08-08 12:59:00 +02:00
Stefan Haller ae1ab5ca1e Extend the raw-diff fallback to the commit/stash/patch-building panels
Wire the remaining diff panels — local commits, sub-commits, commit files,
stash, and reflog — into the focused main view's raw-diff fallback, the same way
the files panel already is: each computes DiffMainViewShouldRenderRaw and renders
its diff raw (no pager) when focused under a pager whose output we can't resolve,
so its selection stays toggleable into a custom patch (or, for reflog, navigable
and copyable).

The commit-diff panels build their diff through the diff helper, which can't
reach the staging helper, so the panel computes renderRaw and passes it in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 10f41bed50 Thread ignoreExternalDiff through the remaining diff-cmd builders
Prep for extending the focused main view's raw-diff fallback beyond the files
panel. ShowCmdObj, ShowFileDiffCmdObj, ShowStashEntryCmdObj and DiffCmdObj gain
the ignoreExternalDiff arg WorktreeFileDiffCmdObj already has — forcing git's own
coloured diff regardless of a configured external diff command. All callers pass
false, so behaviour is unchanged.

Also lift the pty-vs-command task choice out of the files controller into
types.NewMainViewDiffTask(WithPrefix), so the commit/stash/patch-building panels
(whose diffs are built in the diff helper, which can't reach the staging helper)
can select the same raw-fallback task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 2a69d4c140 Fall back to the raw diff when focusing a main view under an unresolvable pager
The focused main view is the staging surface, so it has to resolve the diff it
shows to patch-space. A pager that restructures the diff without emitting our
metadata (stock delta-default, plain difftastic, `cat -n`) produces output the
buffer parser can't read, which would leave its diff unstageable.

When such a diff is focused, re-render it raw — git's own colour, no pager — the
same content a no-pager setup shows, which the buffer parser handles. Browsing
keeps the pretty pager output; only focusing to act switches to raw.

Whether the pager is usable is decided by probing it: run it on empty input and
look for the version-only OSC 1717 handshake a metadata-aware pager emits first.
This is a pager-level, content-independent fact (a binary file, which has no
change lines under any pager, can't mislead it) and it's known before we render,
so we never render pretty only to discover mid-flight that we should have rendered
raw. The verdict is cached per pager (reset when the pager changes). No PTY is
needed — git needs a terminal to decide to invoke a pager, but the pager emits the
handshake regardless. A git-config external diff driver (useExternalDiffGitConfig)
is chosen per file via .gitattributes and a single diff can mix drivers, so
there's no one pager to probe; it's treated as unsupported (always raw).

Bypassing the pager needs two things, since a pager reaches the diff by two
routes: an external diff command (suppressed in the cmd via the new
ignoreExternalDiff arg, keeping git's colour, unlike plain) and a stdin pager
(GIT_PAGER, applied by the pty task — so the raw render uses a plain command
task instead).

This wires the files panel; the commit/stash/patch-building panels follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 482acff66d Session notes: (C) ctrl+o copy done (§21.28)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller f0a997705a Copy the selected diff line(s) from the focused main view
`ctrl+o` in the focused main view copies the selection to the clipboard,
mirroring the staging view's copy. Unlike stage / discard, copying is the same
for every diff panel — it just reads the text shown — so it's a direct
MainViewController command rather than a FocusedMainViewActions method. That
keeps it free of per-panel duplication and lets it work over panels that have
no actions, notably the reflog.

The +/- prefix handling is metadata-aware where it matters: with no pager the
main view shows the raw diff, so a homogeneous selection has its +/-/space
column stripped (dropDiffPrefix) to ease pasting into code. With a pager
configured the rendered lines carry no such column, so stripping a leading
character would eat real content — copy verbatim instead. (Refining the raw
case to classify header lines via the diff-line metadata, fixing the
'--- a/file' edge case noted in 159bbb0825, is left for later.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller a0af1b1d9a Session notes: discard bug fixes + selection visibility (§21.27)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 252a26cbc4 Hide the focused main view's selection when there's no diff to act on
The focused main view always showed a selection while it held focus, even when
its content was a placeholder ("No changed files", a merge-conflict message)
rather than a diff — so pressing a navigation key conjured a highlighted line
over "No changed files", and discarding the last change (or changes vanishing
outside lazygit) left a stale selection behind.

A selection should only show when there are change lines to act on. Two
moments establish or change the content:

- Focus: showInitialDiffSelection now leaves the selection off when the view
  has no change lines (ViewHasChangeLines), rather than highlighting a stray
  line at the top.
- Render: the side panel's render-to-main is where it decides between a diff
  and a placeholder, so that's where the selection's visibility is set —
  updateFocusedMainViewSelectionVisibility shows it only on the focused pane
  and only when a diff is being rendered. This covers the refresh cases that
  focus can't: discarding the last change, and changes disappearing or
  reappearing outside lazygit, all hide or restore the selection on the next
  refresh. (Focusing reuses the already-rendered content rather than
  re-rendering, which is why focus needs its own check.)

Wired into the files panel, where placeholders occur; the commit panels always
render a diff. Adds a SelectionIsShown/SelectionIsHidden test assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 26dcab23ae Session notes: (B) d discard a hunk done (§21.26)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller e9c4425df6 Discard the selected diff line(s) from the focused main view
Wire the focused main view's `d` binding to a DiscardSelection action on the
side panel beneath, the discard counterpart of the primary action. As with
stage/toggle, what discard means is the panel's business:

- the files panel discards from the working tree, mirroring the staging
  view's `d` — reverse-apply not cached on the unstaged side (destructive, so
  confirmed), reverse-apply cached on the staged side (i.e. unstage);
- the commit panels (commit files, and the whole-commit diff of the local
  commits / sub-commits / stash panels) remove the selected lines from the
  commit via a rebase, mirroring the patch builder's "discard lines from
  commit" — building a one-off patch from the selection and deleting it.

The commit-discard backend and its guard are shared free functions in
patch_building_from_main_view.go, paralleling the patch toggle. Discarding
from a commit is only possible on a local branch, so DiscardSelection grows a
disabled-reason companion on the FocusedMainViewActions interface: the `d`
binding is greyed with a reason on stash and other-branch sub-commits (never
rebaseable) and while a rebase is in progress, exactly as the patch builder
greys it. The files panel is always available (a zero-context diff is the one
error it reports inline, matching the staging view).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 99f80b1bc8 Generalize FilesController.stageDiffLines to applyDiffLines
The focused main view's discard command (coming next) needs to read either
side's diff and apply a patch with an arbitrary direction and target: discard
from the working tree reads the unstaged diff but applies a reverse patch that
is not cached. stageDiffLines couldn't express that — it used a single reverse
flag both to pick which diff to read and to reverse the apply, and hardcoded
Cached: true.

The read side and the apply direction are independent (they coincide only for
staging and unstaging), so split them: applyDiffLines now takes sourceCached
(which diff to read) and a git_commands.ApplyPatchOpts (how to apply). Renamed
from stageDiffLines since it now covers stage, unstage, and discard. Staging
passes the same values as before, preserving behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 2d5d149f5e Session notes: (A) dispatch refactor done (§21.25)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 88dae2f2c6 Collapse the focused-main-view handler channels into one
Each command a side panel handles in the focused main view needed its own
delegation channel: a HasKeybindings getter, an IBaseContext Add method, a
BaseContext field + getter, a baseController nil default, and an attach.go
registration — roughly five touch points per command. With three commands
(click, stage, toggle-patch) that was already a lot of boilerplate, and it
doesn't scale to the discard / copy commands coming next.

Replace the three channels with one: a side panel exposes a single
FocusedMainViewActions interface via GetFocusedMainViewActions (nil when its
diff offers no actions), and the controllers implement that interface
directly. The stage and toggle-patch handlers, already unified to a plain
error return, become one PrimaryAction method whose meaning is the panel's
business (stage for the files panel, patch toggle for the commit panels);
the click handler becomes OnClick. MainViewController is now a thin
dispatcher: fetch the actions from the panel beneath, call the method.
Adding a command is now one interface method, an implementation in the two
or three controllers, and a keybinding — no plumbing.

The toggle-patch channel did double duty as the "this panel builds a custom
patch" signal for the inclusion gutter (shown only beneath a patch-building
panel, not the staging files panel). Collapsing the channels removes that
proxy, so make the classification explicit on DiffMainViewContext, whose
marker method now returns a DiffMainViewType (none / staging / patch-building)
instead of being a bare marker. The gutter shows only beneath a panel whose
type is patch-building (commit files, local commits, sub-commits, stash).

Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 96b09bb41d Push the staging focus-follow into the side-panel handler
The focused main view's space binding delegates to the side panel beneath
it, but the two handlers were asymmetric: the patch-toggle handler did its
own re-render and re-established the selection itself, while the staging
handler returned a focusViewName so the dispatcher (stageRange) could do the
reveal-and-focus dance on its behalf. That split blocks collapsing the
per-command handler channels into one, since the two have different return
types.

Make staging match the toggle: GetOnStageFocusedMainView returns plain
error, and the FilesController handler does the post-staging reveal and pane
focus itself. The reveal/select-mode logic the two handlers shared (collapse
a range to a line, preserve the change-line ordinal across the re-render,
re-expand a hunk) is extracted into revealSelectionAfterPrimaryAction, with
mainContextForViewName resolving a main view name to its context. The
dispatcher is now uniform: read the selected range, hand it to whichever
handler the panel registered.

Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 1b02020022 Session notes: revised forward plan (§21.24) — drop steps 7/8, add commands + dispatch refactor + pager fallback
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 4fe5211da0 Session notes: crash fix for resetting a patch built from the commits main view
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller f4e180c128 Session notes: step 6c done (patch building from the whole-commit main view)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 86ce3ba798 Build a custom patch from the commits / sub-commits / stash main views
Until now space toggled lines into a custom patch only from the commit files
main view (the per-file diff). Register the same toggle handler on the commits,
sub-commits and stash panels, so a patch can be built straight from the
whole-commit (multi-file) diff their main view shows, without first diving into
the commit files panel.

The handler lives on SwitchToDiffFilesController, which is already bound to
exactly those three panels and already knows how to derive the patch target
(from/to/reverse/canRebase) for the selected ref — pulled out of enter() into a
shared canRebase helper and the reused FromAndToForDiff. The post-toggle refresh
is the cheap one: these panels have no per-file patch indicator to update, so we
just re-render their own main + secondary views rather than reloading the whole
commit list on every keystroke.

Sub-commits and stash didn't render the secondary patch view at all; give them
the same secondaryPatchPanelUpdateOpts the commits panel uses, and recompute the
inclusion gutter as their diff (re-)renders, so the cumulative patch and the
gutter track the toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 26b27ed69b Extract the patch-toggle back end out of CommitFilesController
The next commit adds patch building from the commits / sub-commits / stash
main views, which toggle the same way as the commit files main view; only the
patch target and the post-toggle refresh differ. Pull the panel-agnostic back
end (the toggle skeleton, the line toggle, the post-toggle reveal, the patch
filename mapping) into shared free functions so both controllers reuse it.

The skeleton now takes the patch target (from/to/reverse/canRebase) and a
refresh callback as parameters rather than reading them from the commit files
context and hardcoding Refresh({COMMIT_FILES}). The commit files handler passes
exactly what it did before, so behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller e074c40cc9 Session notes: first interactive feedback on the gutter (4 of 5 fixed)
Records the visibility model (gutter visible iff patch-active AND main
focused), the selection re-select after the split re-render, the
all-wrapped-segments marker, and the deferred pager-switch bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller c14d9432cb Session notes: toggle refreshes normally; scroll preserved for free
Records the fix for the §21.20 live-update gap: the patch toggle now
refreshes (updating the browser indicators + secondary patch summary +
the split), scroll/selection are preserved automatically because the
re-render uses the same diff command, and the gutter rides the same
re-render via the pipeline's own content-equality test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 115582e9bc Session notes: step 6 built — inclusion gutter + patch toggle (6a/6b)
Records what landed, the confirmed sync (no re-render) insight, what
needs interactive sign-off, the known limitations, and the 6c/step-7
next steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 67ba1899b4 Build a custom patch from the focused main view of a commit's files
Pressing space in the commit-files focused main view now toggles the
selected line(s)/hunk into or out of the custom patch, instead of being a
no-op. It's the patch-building counterpart of working-tree staging from
the main view: the selection is resolved to change-line identities the
same way (ChangeLinesInViewRange), then mapped to the patch builder's
per-file line indices and added or removed.

The crux is that the commit's diff is unchanged by a toggle — only the
inclusion set changes — so unlike staging there's no async re-render to
ride. Membership is shown by an on-demand inclusion gutter: a reserved
left column painted with a marker on every change line currently in the
patch, recomputed synchronously after each toggle and on focusing the
main view, over the existing pager output. This is what lets patch
building work over a restructuring pager at all, where the old green
first-char overlay (which rewrites the rendered bytes) can't.

space routes to staging or patch building based on which handler the
panel beneath registers, via a new onTogglePatchFocusedMainView channel
(kept separate from staging because the post-action differs: sync gutter
repaint vs async reveal). Scoped to the commit-files panel for now;
commits/sub-commits/stash and the whole-commit multi-file diff follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 2fee7653f2 Extract stageRange from stageSelectedLine in the main view
Prep for routing space to custom-patch building when the panel beneath
the focused main view is a commits/commit-files panel. stageSelectedLine
becomes the router; the staging-specific body (async re-render + reveal)
moves to stageRange. Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 4c123f9414 Add identity-based line accessors to PatchBuilder
The patch builder works in patch-line-index space, which the patch
explorer can supply directly from its parsed-patch state. The focused
main view can't: it only knows a selection as diff-line metadata
identities (file line number + deletion?), and those indices differ
between the raw diff and however a pager renders it.

Add the conversion at the boundary so the main view stays in identity
space: PatchLineIndicesForLines maps identities to indices (for
toggling) and IncludedLineIdentities reports the included change lines
as identities (for the inclusion gutter to match rendered rows against).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 12523c8a20 Add an on-demand inclusion gutter to gocui views
A reserved left-hand column that draws a per-line marker glyph and shifts
the content right past it, for the custom-patch inclusion overlay (which
needs to show which change lines are in the patch over arbitrary pager
output, where it can't touch the rendered bytes).

It's pure draw-time decoration: the content buffer, and so the diff-line
metadata, click resolution and wrapping inputs, are untouched. The marker
is drawn on a line's first wrapped segment only, and the gutter narrows
the content wrap width while shown. Off by default; SetInclusionGutter
turns it on per view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 01882cf2fd Session notes: step 6 design — the on-demand inclusion gutter
Records the plan for patch-building from the main view: the sync (no
re-render) vs async (staging) split, the included-set mapping, the
user's gutter decisions (on-demand, inclusion-only, checkmark), the
draw-time gocui rendering approach, and the 6a/6b/6c decomposition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 22e1c82405 Session notes: prototype-branch callout; step 5 signed off; step 6 next 2026-08-08 12:59:00 +02:00
Stefan Haller 2421e19f5d Session notes: reveal reworked to preserve change-line ordinal 2026-08-08 12:59:00 +02:00
Stefan Haller cf89ef9d4f Preserve the change-line ordinal in the post-stage reveal
The reveal advanced the selection by capturing the next/previous change as a
patch identity and finding it again after the re-render. No line-number
identity is stable and unique across a stage, though: a deletion shares its
new-file number with the rest of its block (so staging one deletion of a block
landed back on the block's first line), and in the staged pane the new-file
number is the index, which staging/unstaging shifts (so unstaging a
modification's deletion missed its replacement line and fell back to an earlier
block). The previous worktree-line and adjacent-change-line matching each fixed
one case and left another.

Do instead what the staging view does: preserve the selection's ordinal among
change lines. Read the acted-on line's ordinal from the pane it was in before
the op; after the re-render, select the change line at that ordinal in the
target pane (clamped to the last). Since the op removes the acted-on change
line(s), that ordinal then holds the next surviving change — the next line of
the same block, or the next block when a whole block was staged. This needs no
reasoning about which side's line numbers are stable, so it handles deletions
and the staged pane uniformly.

The matching machinery (matchByWorktreeChange, AdjacentChangeLine) is gone; the
identity-matched restore (escape, -U preserve) and the new positional restore
now share installDiffLineRestore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 133dabb394 Session notes: line-mode reveal advances to next line, not next block 2026-08-08 12:59:00 +02:00
Stefan Haller f461378598 Advance to the next change line, not the next block, in the reveal
The post-stage reveal picked its primary candidate with AdjacentChangeBlock,
which skips the rest of the acted-on line's block. That's right in hunk mode,
where the whole block is staged, but wrong in line mode: staging the first
line of a multi-line block left the rest of the block in place, yet the
selection jumped past it to the next block instead of landing on the next
(now first) line of the same block.

Use AdjacentChangeLine — the first change line strictly after/before the
selection — for the next/previous candidates. It unifies both modes: after a
whole block (whose last line is followed by context) the next change line is
the next block's, so hunk mode is unchanged; after a single line the next
change line is the next line of the same block. place stays mode-aware
(hunk-expand vs single line), so nothing else changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller df09df1940 Hack: inset the selection highlight by 3 in the main view
This makes it possible to see what lines delta renders as red or green.

It's not pretty, but makes testing easier for now. Needs a better
solution eventually.
2026-08-08 12:59:00 +02:00
Stefan Haller 377a352075 Session notes: rebased onto wrapped-fill branch; blank-line metadata + reveal worktree-match fixes 2026-08-08 12:59:00 +02:00
Stefan Haller dcdc919a0e Match the post-stage reveal by worktree line, not old-file line
The reveal that advances the focused main view's selection after staging
captured the next/previous change block as a patch identity and found it
again in the re-rendered diff via SamePatchLine. SamePatchLine keys a
deletion on its old-file (index-side) line number, which staging shifts:
staging a hunk that changes the line count moves the index-side numbers of
every hunk below it. So a deletion-led "next hunk" candidate no longer
matched, and the reveal fell back to a worse candidate — often colliding
with a header or context row (SamePatchLine doesn't require a change line)
and, in hunk mode, snapping to the first change block. The visible result
was the selection jumping to an *earlier* hunk after staging.

Match the reveal's candidates by their worktree (new-file) line number,
which staging never moves, and require a change line of the same side. This
is safe here because the reveal only ever targets change blocks and
selectHunkAround expands to the whole block, so the new-file number's
ambiguity between two consecutive deletions (the reason SamePatchLine uses
the old-file number) doesn't matter.

The escape restore and the -U context-size preserve keep SamePatchLine:
they re-render the same staged/unstaged state, so no index-side shift, and
the latter deliberately anchors on context lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 131a315b7f Keep a metadata-carrying cell for a blank changed line
delta, fed a colored diff, renders some blank changed lines (an empty
deleted/added line) as just the OSC 1717 metadata followed by an empty
line — either with no fill at all, or as a background plus an ESC[0K that
the buffer now turns into a draw-time trailing fill rather than content
cells. Either way the line ends up with no cells, so its metadata had
nowhere to live and the line resolved to "not a change".

In the focused main view that broke change-block detection: a blank
deleted line in the middle of a hunk split it in two, so pressing 0
selected only the lines above the blank and the down arrow jumped to the
rest below it.

Restore the content-less sentinel cell finishLine used to add, but only
when there is pending OSC metadata to carry — so an empty line stays
resolvable as the change it is, without reintroducing the unconditional
sentinel (and its prevFgColor coupling) that was removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller 2088408f43 Session notes: step 5 part 2 done — focus follows the acted-on side to the right pane 2026-08-08 12:59:00 +02:00
Stefan Haller 05c7b82a84 Follow the acted-on side to the right pane after staging from the main view
After staging or unstaging from the focused main view, focus stayed on the
pane the user acted in even when the side they were acting on moved to the
other pane. Two cases got it wrong:

- Unstaging the first hunk of an only-staged file splits the diff: the main
  half flips to show the just-unstaged change, and the staged remainder jumps
  to the secondary half — but focus stayed on the main half, away from the
  staged content the user was working through.
- Unstaging the last staged hunk from the secondary half empties the staged
  side and collapses the split, hiding the secondary half — leaving focus
  stranded on a hidden pane.

The rule is the same in both: focus the staged side while it survives. The
stage handler now reports which focused-main pane should hold focus —
the secondary half when unstaging leaves the file split, the main half
otherwise — and the controller re-selects the revealed change in that pane
and focuses it. The split is read from the model, which Refresh has already
updated synchronously by the time the handler returns; the re-render it
queues is what the reveal rides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller a2e1254a96 Let the post-stage reveal target a different pane than the one acted on
Staging or unstaging can move the acted-on side to the other half of the
focused main view — unstaging the first hunk of an only-staged file splits
it, pushing the staged remainder to the secondary pane. The reveal that
re-selects the nearest surviving change after the op then has to ride the
re-render of that other pane, not the one the user acted in.

Split RevealSelectionAfterStaging's single view into a source (where the
candidate lines are read) and a target (where they're found again and
re-selected), and get-or-create the target's buffer manager so the restore
can be installed on a pane that hasn't rendered yet. Behavior-preserving:
the sole caller passes the same view for both, as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00
Stefan Haller e58f6d43ad Session notes: part 1 signed off; part-2 focus switch unifies with only-staged unstage bug; deleted-file note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:58:59 +02:00
Stefan Haller c2dc9c329f Session notes: step 5 part 1 (post-stage reveal) done; part 2 (empty-pane focus switch) scoped
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:58:59 +02:00
Stefan Haller 4882760690 Advance the selection to the next change after staging from the main view
Staging or unstaging a selection re-renders the focused main view's diff (the
staged lines move to the other side), which left the selection at a now-stale
view position — often off the new, shorter content, so it looked like there was
no selection at all.

Install a restore before the re-render that lands the selection on the change
nearest the one just acted on, the same way the staging view advances: the
change block after the selection, else the one before it, else the acted-on line
itself (which only survives when the whole side was staged and the view flips to
the other side). It re-selects in the current mode, so hunk staging walks hunk to
hunk. A range selection collapses to a single line first, since staging consumes
it. This rides the existing restore-by-identity machinery (as the escape restore
does), so it works over any conforming rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:58:59 +02:00
Stefan Haller 01b2f621f9 Session notes: step 3-4 signed off (SxS works incl. difftastic); unstage focus issues -> step 5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:58:59 +02:00
Stefan Haller 8b04f49c2b Session notes: step 4 done (SxS multi-record + multi-file staging)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:58:59 +02:00
Stefan Haller 4a33b42d19 Stage both sides of a side-by-side row from the focused main view
A side-by-side rendering (patched delta in side-by-side mode) carries more than
one diff-line record per rendered row: the deletion on the left, the addition
replacing it on the right. DiffLineContent.Metadata keeps only the first payload
— enough to identify a single-column row, but it drops the right side — so a
range staged from such a view would miss half the changes.

Add a gocui accessor for all the distinct metadata payloads a row's cells carry,
and have the range collector resolve every one of them, so staging a
side-by-side row includes both sides. You can't stage just one side of a
side-by-side row; that's an accepted restriction (switch to a single-column
rendering to do it). Rows without metadata (no pager, or the buffer-parse /
hyperlink backends) keep falling back to their single resolved record, so
single-column staging is unchanged.

The full delta-side-by-side chain can't run in the integration harness (no real
pager), so it's covered by a gocui unit test for the multi-payload accessor;
end-to-end behaviour needs interactive verification with the patched delta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:58:59 +02:00
Stefan Haller a5a0506913 Stage a selection spanning several files from the focused main view
The focused main view of a directory node shows a multi-file diff, so a range
selected in it can span more than one file. Group the selected change lines by
file and apply one patch per file, logging the action and refreshing once around
the whole batch. The stage/unstage direction is uniform — the whole diff is
rendered on one side — so it's decided once for the batch.

Each change line is mapped back to its file by the path its diff-line metadata
carries, which also lets a single-file selection go through the same path
instead of relying on the selected node being the file; staging from the focused
main view no longer bails on a directory selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:58:59 +02:00