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>
This commit is contained in:
Stefan Haller 2026-06-20 14:06:44 +02:00
parent f3ef02556a
commit c0ee7de776
2 changed files with 50 additions and 0 deletions

View file

@ -485,8 +485,10 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
case stateOSCMetadata:
switch {
case characterEquals(ch, 0x07):
ei.dropMetadataIfHandshake()
ei.state = stateNone
case characterEquals(ch, 0x1b):
ei.dropMetadataIfHandshake()
ei.state = stateOSCEndEscape
default:
ei.metadata.Write(ch)
@ -507,6 +509,18 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
return false, nil
}
// dropMetadataIfHandshake discards a just-completed OSC 1717 payload that carries no
// fields (no ';'). A metadata-aware pager emits such a version-only record once, as
// its first output, to announce it speaks the protocol (so we can probe it; see the
// raw-diff fallback in the lazygit staging helper). It isn't per-line metadata, so it
// must not linger in the accumulator and attach to the following line. Per-line
// payloads always have fields, so they're kept.
func (ei *escapeInterpreter) dropMetadataIfHandshake() {
if !strings.Contains(ei.metadata.String(), ";") {
ei.metadata.Reset()
}
}
func (ei *escapeInterpreter) outputCSI() error {
n := len(ei.csiParam)
for i := 0; i < n; {

View file

@ -217,6 +217,42 @@ func TestDiffLineMetadataPayloads(t *testing.T) {
}, v.DiffLineMetadataPayloads())
}
func TestDiffLineMetadataHandshakeSwallowed(t *testing.T) {
v := NewView("name", 0, 0, 80, 10, OutputNormal)
// A metadata-aware pager emits a version-only handshake (an OSC 1717 with no
// fields) as its first output, immediately before the diff, to announce it speaks
// the protocol. It must be swallowed whole: no visible bytes, no phantom line, and
// crucially it must not attach as metadata to the diff header that follows it.
osc := func(payload string) string { return "\x1b]1717;" + payload + "\x1b\\" }
v.writeString(osc("1") + strings.Join([]string{
"diff --git a/foo.txt b/foo.txt",
osc("1;a;1;;foo.txt") + "added",
}, "\n"))
// The handshake produced no phantom line and no visible bytes.
assert.Equal(t, []string{
"diff --git a/foo.txt b/foo.txt",
"added",
}, v.BufferLines())
// The handshake didn't bleed onto the header line, and the real per-line metadata
// after it still applies.
type result struct {
payload string
ok bool
}
got := make([]result, len(v.buf.lines))
for y := range v.buf.lines {
payload, ok := v.DiffLineMetadataInLine(y)
got[y] = result{payload, ok}
}
assert.Equal(t, []result{
{"", false},
{"1;a;1;;foo.txt", true},
}, got)
}
// When a re-render produces fewer view lines than the previous one,
// refreshViewLinesIfNeeded must truncate viewLines to the new content. If it
// didn't (it used to overwrite in place and keep the tail), a reader could map a