Skip to content

Prototype: Fold the staging functionality into the main view - #5732

Draft
stefanhaller wants to merge 201 commits into
masterfrom
fold-staging-functionality-into-main-view
Draft

Prototype: Fold the staging functionality into the main view#5732
stefanhaller wants to merge 201 commits into
masterfrom
fold-staging-functionality-into-main-view

Conversation

@stefanhaller

@stefanhaller stefanhaller commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

This is a prototype of a major change to how staging works:

  • there is no longer a separate staging (or patch-building) panel, this is now done directly from the main view
  • for this purpose, the main view gets a selection when focused, which works the same way as the one in the former staging view
  • this works even when a pager is used, as long as the pager supports the new OSC protocol described in https://github.com/stefanhaller/diff-line-metadata-spec
  • many other things that I forgot to list here.

In order to illustrate this better I made a video to demo it. Unfortunately it got a little longer than I had planned, I'm not experienced in producing content like this.

Prototype branches for the three mentioned pagers are here: delta, diff-so-fancy, difftastic.

For feedback on the OSC spec, please comment on stefanhaller/diff-line-metadata-spec#1, not here.

@scottchiefbaker

Copy link
Copy Markdown

I'm about 8 minutes into your demo video and I'm already very excited. Conceptually it seems simple enough. I just need to add some "hidden" escape codes into the d-s-f output to mark changed lines?

@scottchiefbaker

Copy link
Copy Markdown
  1. Where feedback is most wanted
  1. The OSC number, 1717....

I don't know enough about OSC codes to have a strong opinion on this. But 1717 seems fine to me.

  1. The env-var name and grammar (EMIT_OSC1717_METADATA=V1,…).

This seems fine to me I think.

  1. The token-vs-line mismatch (§8) — should there be an m type, or is host-side inference the right home for it?

Not sure I have enough information to say on this.

  1. Can your pager actually produce all four fields per region?

I believe so. I'd definitely like to work closer with you to beta-test this feature as there are a lot of technical details I don't 100% follow quite yet.

  1. Content-lines-only scope (§5.5) — is anything lost for your pager?

I don't think so. d-s-f parses line by line, and retains header information so I should be able to call back to the last header when I encounter a line change.

@scottchiefbaker

Copy link
Copy Markdown

Having read the spec document and watched your video can I get clarification on a couple of things?

  1. In this new metadata mode lazygit (or whoever) emits an environment variable EMIT_OSC1717_METADATA = V1[,V2,…] when it's in select hunk/line mode.
  2. If a pager sees this environment variable it needs to output a "handshake" as the first part of it's output indicating what version of OSC1717 it understands? "\e]1717;1\e\"
  3. Will each line of context (the lines that are NOT changing) need a OSC1717 record? In the case of d-s-f which does line-by-line output, every output line will have a record?
  4. From section 6.1 "One record per region, at the region's start". In this hypothetical diff which contains: a line add, a line change, and a li ne delete, how many "regions" would that be? 3? 4? Or is region another way of saying "line" in which case there would be 20?
diff --git a/LICENSE b/LICENSE
index eb26539..028c6af 100644
--- a/LICENSE
+++ b/LICENSE
@@ -5,17 +5,18 @@ Copyright (c) 2016 So Fancy team
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

+FOOBAR
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM;
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.

Help me understand the fields for the deleted line. Would it be: "\e]1717;1;d;;8;b/LICENSE\e\"? I don't fully understand old line vs new line.

For the added line. Would it be: "\e]1717;1;a;15;;b/LICENSE\e\"?

In section 4.3 it says "context, new line 10" but I think it would be more clear as "context, line 10" because it's not new.

@scottchiefbaker

Copy link
Copy Markdown

Possible minor hiccup? While attempting to implement the basics of this I came across a minor problem:

# Works fine
perl -E 'print "\e]1717;1\e\\"'
# Outputs `\`
perl -E 'print "\e]1717;1\e\\"' | less

The default mode with d-s-f it to pipe through less as a pager, and it appears less doesn't handle OSC escape sequences super cleanly.

@stefanhaller

Copy link
Copy Markdown
Collaborator Author

While attempting to implement the basics of this ...

I must not have made that clear enough: you don't have to do that, I did already. I just didn't post a (draft) PR yet because I first wanted to get feedback on the spec; but the changes are ready (apart from the necessary changes to the docs, if any, and to the changelog etc.). My branch is here if you want to try it.

it appears less doesn't handle OSC escape sequences super cleanly.

That's too bad, but it's not a problem, because d-s-f will only emit the sequences when it is running inside lazygit.

I'm taking your other questions over to #5731, that's where the spec should be discussed.

@scottchiefbaker

Copy link
Copy Markdown

Oh man... you didn't tell me you had most of the work done in d-s-f. You should have led with that. Your code looks pretty sane to me. Well done.

@stefanhaller
stefanhaller force-pushed the fold-staging-functionality-into-main-view branch 2 times, most recently from ecdd436 to a75c169 Compare June 30, 2026 12:49
@nullromo

nullromo commented Jul 1, 2026

Copy link
Copy Markdown

Just want to say that this prototype looks very exciting and I would be ecstatic if these changes landed in an official Lazygit release.

@jesseduffield

Copy link
Copy Markdown
Owner

This looks amazing. This would be a serious game-changer for my workflow and I imagine many others as well.

@stefanhaller

Copy link
Copy Markdown
Collaborator Author

@dandavison @scottchiefbaker I want to make progress on this. I'm happy with the prototype and with the spec, and I want to start implementing it soon. Anything that is still worth iterating on or discussing before I productionize it?

My plan is to start working on it after the next lazygit release, which is scheduled for August 2. My rough estimate is that it will take between one and two weeks to get it all in; we then have a bit of time to test it thoroughly, and hopefully release it with the September release if all goes well. Which means we'd have to ship delta and d-s-f updates some time in late August so that they are available in time. Does that sound like it would work for you?

@dandavison

Copy link
Copy Markdown

That's fine with me @stefanhaller. I'm running your delta branch as my delta locally (with gitu) and will of course report any issues I encounter. From delta's point of view, I'll initially consider OSC 1717 support to be experimental in the sense that delta won't complain if there are some backwards incompatible changes in the way the feature works. Though delta might of course need help with PRs for evolving the support.

@scottchiefbaker

Copy link
Copy Markdown

@stefanhaller I'm fine with all the decisions we've made so far. I support moving forward with this. Will you be updated your diff-so-fancy branch with these proposed changes?

@stefanhaller

Copy link
Copy Markdown
Collaborator Author

Will you be updated your diff-so-fancy branch with these proposed changes?

@scottchiefbaker The PR is already up to date, it is ready to be merged once the fixups are squashed. I'll do that right now. (Please don't merge it just yet though.)

stefanhaller and others added 28 commits August 8, 2026 12:59
Pager authors reviewing the protocol asked to drop the EMIT_ prefix: the
OSC number already names the protocol, so EMIT_ was redundant. Rename the
handshake variable to OSC1717_METADATA in the spec and in the two places
lazygit advertises it (the pager PTY and the metadata probe).
Records the two rename-handling gaps the rebase onto f84ada4 leaves
for productionization: the previousPath="" conflict resolutions in the
focused-main-view patch code, and the failing renamed_file_whole e2e
(a view-rendering regression in the prototype's patch package, not a
build regression and not the previousPath issue).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view now plays the role the staging panel used to,
so the working-tree commands the staging panel offered — commit (and
its variants) and find-base-commit-for-fixup — need to be reachable
there as well.

Gate them to when the focused main view actually shows the working-tree
diff (DiffMainViewTypeStaging), which is exactly the role the staging
panel filled. Over a commit's or stash's diff these commands would
operate on the working tree, unrelated to what's on screen, so the keys
are a no-op there and the bindings don't clutter its keybinding menu.

The gate is re-checked on each press rather than captured at
registration time, since one keybinding set serves the main view over
every panel. That requires reading the panel beneath the main view from
GetKeybindings (to decide whether to show the descriptions), which runs
for off-stack contexts too — at startup and during cheatsheet
generation — so a panic-safe IsInStack guards the NextInStack lookup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
n / N step through the files of a multi-file diff one at a time, which
is tedious when the diff spans many files. Add `f`, which pops up a menu
listing every file in the diff — in the order they appear, as
repo-relative paths — so you can jump straight to one.

Picking a file reuses the exact landing logic n / N use (navigate to the
file's first row), computed from the same backUpOverHeader machinery
AdjacentFile uses, so the menu and the step keys agree on where each
file begins and land identically.

The menu title and the keybinding description are hard-coded English for
now; this is a prototype.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
We maintain it in the osc-1717-spec branch; I want to avoid confusion as
to which one is newer, so remove it from here.
The spec regained file-header (f) and hunk-header (h) records: f never
carries a line number, h always does (the first line of the hunk it
heads). Accept them in the OSC metadata backend, so a conforming pager's
header rows resolve to the same DiffLineFileHeader/DiffLineHunkHeader
identities the buffer parser already reports for raw diffs.

With header rows located, next/previous file navigation and the
jump-to-file menu land on a file's header row, and header rows become
usable scroll-restore anchors. The consumers need a few adjustments:

- File navigation used to reach a file's top by backing up over the
  untagged rows above its first located row. With tagged headers that
  overshoots onto the blank separator row above the file header, so drop
  the back-up (backUpOverHeader) and land on the first located row
  itself: the header for any conforming source, or the first content
  line under a pager that leaves its headers untagged — an accepted
  degradation for non-conforming pagers, now that the spec makes f/h
  mandatory.

- SamePatchLine now requires headers to match headers of the same kind.
  A hunk header shares its line number with the hunk's first content
  line (and a file header shares "0" with a deleted file's hunk header),
  so without this a position restore aiming at one could land on the
  other. This also applied to raw diffs before, but headers used to be
  unlikely restore targets; now that navigation deliberately lands on
  them, the ambiguity would bite.

- Editing a file-header row opens the file without jumping to a line,
  like pressing edit on a whole file in a side panel. (It used to open
  at line 1 for raw diffs, where headers resolved already.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prototype is complete and signed off; this is the plan for
re-implementing it as a stack of clean PRs off master. It divides the
work into ten PRs (grouped for release-notes value as much as for
technical cohesion), outlines the commits of each, records the scope
decisions made in the planning session (panels removed, enter/dive
gesture dropped, stacked PRs within one release, both extras in scope,
nav/preserve as early standalone PRs), lists everything from the
prototype that must NOT be ported, and carries the sign-off matrix,
separate-lists compatibility seams, and known-gap dispositions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two corrections from review:

- "Pager" is retired in favor of "diff renderer" throughout (the spec
  already made this change); a new mini PR 3 renames the pagers config
  to diffRenderers with migration and docs, and the remaining PRs are
  renumbered accordingly.

- The locked-decision list wrongly said concurrency stays mutex-based;
  the main-thread-mutation rework has landed on master and the
  prototype is rebased on top of it. The plan now states the landed
  threading contract that all PRs must honor, and PR 1 gained a
  re-validate-against-master preamble. Its reset-after-CopyContent
  commit was verified still needed (master's CopyContent still copies
  the source origin); the two locking fixes were reshaped onto the
  viewLines readers master actually has, since the prototype's
  HyperLinkInLine guard belongs to the dropped hyperlink backend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A rendered row can carry several diff-metadata records back-to-back with
nothing painted in between: difftastic's per-hunk banner emits the
file's f immediately followed by the hunk's h, and a modification row
collapsed to a single column emits its d immediately followed by its a
(spec 6.1/6.2). The escape interpreter kept only one accumulating
payload and reset it when the next record started, so every record but
the last was silently dropped -- the banner resolved as a bare hunk
header, and staging a collapsed modification row staged only the
addition half.

Orphan an unconsumed payload instead of dropping it: when a new record
starts (or the line ends) before any cell consumed the current payload,
hand it to the write loop, which materializes it as a content-less
zero-width carrier cell -- the same trick finishLine already used for
delta's metadata-only blank lines, now generalized. Carrier cells hold
their place in left-to-right payload order and are invisible: drawing
paints a transient space that the next cell immediately overwrites at
the same x, and their zero width keeps wrap accounting unchanged.

DiffLineMetadataPayloads thus reports every record of such a row, so
ChangeLinesInViewRange stages both halves of a collapsed modification
row; DiffLineMetadataInLine reports the first (the f of a banner, the d
of a collapsed row), matching the two-column convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A parallel session committed the zero-width OSC-record carrier
(fe80228) and diagnosed the secondary patch pane's removal as broken
under difftastic: the ordinal bridge assumes displayed change lines
match the patch's change lines in order and multiplicity (difftastic
reorders d/a per hunk and collapses modification rows), and an external
diff tool over the temp trees emits the literal a/-b/ paths in its
records, so the patch-builder file lookup no-ops. PR 8 commit 7 now
prescribes the identity bridge (all payloads per row, matched against
identities from the raw temp-tree diff, k-th match -> included[k]) and
records the path-leak as an open sub-item to decide with the user; PR 4
commit 1 references the now-committed carrier, which the bridge relies
on for the d half of collapsed modification rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review clarified that "deferred" in the gap table meant "not addressed
while prototyping", not "optional". Five items are mandatory:

- IsSingleHunkForWholeFile (PR 5 commit 3): regression vs master
  otherwise. Single-file-ness comes from the side panel selection; the
  single-block computation's lean is patch-space over the raw diff
  fetched at focus, with ReadToEnd-on-focus as the accepted fallback.
- Nav targets beyond the loaded content (PR 5 commit 6): ReadToEnd and
  re-scan like openSearch; free if the above reads to end on focus.
  Covers the jump-to-file menu too.
- Copy (PR 5 commit 9): reframed — copy the corresponding raw diff
  lines, never the renderer's output. Dissolves the can't-tell-if-the-
  renderer-preserves-the-prefix-column problem instead of solving it.
- Deleted-file MD-vs-D (new PR 7 commit 5): staging a deleted file's
  entire content stages the file deletion itself.
- Renderer switch shifting the inclusion checkmarks mid-build (new
  PR 8 commit 10): recompute from the new buffer at render completion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A click is reported on the button press, but the press never set the
event's modifier, so a modified click (alt/shift/ctrl+click) reached
handlers stripped of its modifier, indistinguishable from a plain
click. An earlier fix for this ("Carry the keyboard modifier on mouse
click events") was lost while rebasing onto master's mouse gesture
handling (#5854) — and that work also changes what the right shape is:
it isn't enough for the modifier to ride the press alone.

Drag events and the release are delivered to the view that owns the
gesture, and bindings match modifiers exactly. If the press carried alt
but the drags carried plain ModMotion, an alt-press that no binding
consumed would start matching unmodified drag bindings mid-gesture
(drag-select), and the release of a modified gesture would look like a
plain one. So snapshot the modifiers at press time and stamp them on
every event of the gesture: the press, each drag event (combined with
ModMotion), and the release; modifier changes while the button is held
don't alter the gesture. TestMouseReleaseDoesNotKeepPressModifiers
asserted the opposite for the release and is inverted accordingly.

As before, this means a modified click that nothing binds is a no-op
rather than silently acting as a plain click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prototype was rebased onto master past #5854 (gocui mouse gestures)
and #5870 (diff-renderer config rework), so the SHAs quoted in the plan
now resolve only on the pre-rebase copy at
fold-staging-functionality-into-main-view-plan; note that in §1 and in a
dated log in §10.

PR 3 is done: #5870 landed the rename with a bigger config restructure
than planned (per-entry 'type' field, unified 'command', 'rawGit' type,
DiffRendererConfigManager). Rewrite the PR 3 section as a summary of
what landed and its consequences, key the affected decisions in PRs 4-7
off GetDiffRendererType(), add rawGit as a new case for PR 7's raw
fallback, and resolve the open question about the 'pager:' field name.

PR 10 commit 3 is superseded: the rebase dropped the press-only
modifier fix, and #5854's gesture model makes press-only insufficient
anyway; the re-implemented prototype commit carries the press-time
modifiers through the whole gesture, and the plan now points at that
shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
We stopped updating these notes once the real work moved to
focused-main-view-notes.md and the production plan, but two things in them
have since become actively misleading rather than merely stale: they say in
five places that the protocol is content-lines-only, and they use the env
var name from two renames ago.

Mark the document historical, name the spec as the authority, and correct
§11's outcome banner in place rather than rewriting it, so the reasoning
that led to dropping `f`/`h` is still on record next to the reason it was
reversed -- which is the very cost that banner judged acceptable.

Don't touch the `EMIT_OSC1717_METADATA` spellings further down: they are
what those prototypes were built with, and the status block now says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
git now emits the diff line metadata records itself, for the word-diff
formats -- the ones whose output can't be read back from its own text,
which is the same reason we need records out of delta and difftastic. So
a rawGit renderer configured with --color-words is no longer a diff we
have to give up on: it names every row it shows, as a patched pager does.

Two things stood in the way. The probe only knew how to ask a stdin
filter or an external diff driver and reported false for anything else,
so git's own records were never looked for; and the focused main view
treated a rawGit renderer with arguments as unresolvable by definition,
re-rendering it raw whatever the probe said.

So probe git the way we will run it, with the renderer's own arguments,
and look for an actual record rather than the handshake the other probes
settle for. git annotates only some of its formats, so an installed git
that doesn't speak the protocol and arguments that select no word diff
both leave us without records, and looking for a record answers both at
once. Arguments that aren't a word diff (-U10, --stat) therefore keep
rendering raw when focused, as they did before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A rawGit diff renderer needs no pty -- git renders the diff itself, and
only a pager needs a terminal to be spawned at all -- so newPtyTask hands
that case straight to newCmdTask. But the OSC1717 advertisement was set
forty lines further down, past that early return, so git was never asked
to annotate its output and the word-diff renderer we just started
trusting emitted no records.

Set it before the branch, next to LAZYGIT_COLUMNS, which is there for the
same reason. Nothing else changes: a renderer that doesn't know the
variable ignores it, and git says nothing for the formats it doesn't
annotate, which is every format a rawGit renderer without word-diff
arguments produces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Probe git for the handshake like the other renderers, not for a record.

git announces itself for exactly the formats it describes, so asked with
the renderer's own arguments the handshake already answers the question,
and on empty input at that. Looking for a record was over-specified and
made git the one renderer probed differently for no reason.

The two probes that now both stand a diff up from two empty temp files
share that setup.
The plan assumed git would never emit the records, so PR 4 skipped the
advertisement for rawGit renderers and PR 7 listed "how should rawGit
entries decide the raw fallback" as undecided, with a static args-present
rule or a well-formedness gate as the candidates. git can emit them now,
which answers that question better than either candidate: probe a rawGit
entry like any other renderer and the fallback collapses to a single rule
for every renderer type.

Record the two traps as well, since neither is visible from the outside --
the advertisement has to precede newPtyTask's no-pty early return, and the
probe's cache signature has to include the args.

Say explicitly that none of this waits on git upstream. The probe asks the
installed git what it can do, so a stock git degrades to exactly the
behaviour of a non-conforming renderer, and a fork covers the users who
want the feature if the patch is never accepted.
Two of them constrain later work: TaskOpts.ResetOrigin replaces the planned
LinesToRead.ResetOrigin that PR 6 was going to extend, and the screenColMax
gap PR 1 had to fix is still live on the prototype branch that later PRs
transcribe from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The regression it found retires the PR 6 note from deviation 6, and the
race fix establishes where unrelated bugs found during this work go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deviation 6 still described the TaskOpts field as the shape PR 6 should
build on while deviation 8 said it was gone, so a reader hitting 6 first
would follow a design that no longer exists. Deviation 6 now describes the
final shape and what it means for PR 6, and PR 6's own commit 1 says the
same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two findings from the interactive pass were folded back, which took the
loading-indicator gate to before the off-screen render — where the blanking
would otherwise arrive — and made the amend! for the origin reset
unnecessary, since the flag it needs now already exists by then.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
resetOriginPending was the old name that we initially used, it has been
renamed to newContentPending meanwhile.
@stefanhaller
stefanhaller force-pushed the fold-staging-functionality-into-main-view branch from ad54705 to 40bf5fb Compare August 9, 2026 13:46
@Esgariot

Esgariot commented Aug 9, 2026

Copy link
Copy Markdown

Hey, I randomly stumbled upon this PR and read your draft of OSC1717, and found it very interesting! I've never used lazygit (, but I've used jjui which also is able to embed the output from diff tools), and I was wondering about few things:

Would the workaround, that doesn't introduce a new OSC, be a parser that is able to map rendered output (e.g. the red/green background, purple vertical lines and filename between those) and tries to reconstruct the unidiff format from these rendered lines, or is even that too late and lazygit doesn't even receive the escape codes to parse? (I know trying to parse the background code would be totally hacky and only fit for single diff tool at a time, just asking hypothetically so I could understand the process better.)

Also, in the core concept, does this OSC1717 generalize to "I need to read some hidden text that happens to be the hunk markers so terminals can render the pretty diff and lazygit can render it too BUT ALSO read the hidden metadata"? Is coupling that OSC to unidiff representation the way to converge on the common standard faster or is there some limitation that makes tying the OSC to concrete set of text markers the only way forward?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants