diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 5ca52921..c1122547 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -701,7 +701,9 @@ export function normalizeScrollbackEol(buffer: string): string { */ export function joinScrollbackAndFrame(scrollback: string | undefined, frame: string): string { if (!scrollback) return frame; - return `${scrollback.replace(/\n$/, '')}\n${frame}`; + // Two separate captures: tmux encodes the frame relative to the DEFAULT + // state, so the scrollback's last row must not leave its attributes active. + return `${scrollback.replace(/\n$/, '')}\x1b[0m\n${frame}`; } /** Pane geometry and caret position, as `display-message` reports them. */ diff --git a/test/remote-pane-history.test.ts b/test/remote-pane-history.test.ts index 144df6cc..4b5ca3f8 100644 --- a/test/remote-pane-history.test.ts +++ b/test/remote-pane-history.test.ts @@ -146,8 +146,15 @@ describe('the remote script under a real shell', () => { describe('joinScrollbackAndFrame', () => { it('puts the scrollback rows above the frame in tmux’s own row shape', () => { - expect(joinScrollbackAndFrame('old 1\nold 2\n', 'frame 1\nframe 2\n')).toBe('old 1\nold 2\nframe 1\nframe 2\n'); - expect(joinScrollbackAndFrame('old 1', 'frame 1')).toBe('old 1\nframe 1'); + expect(joinScrollbackAndFrame('old 1\nold 2\n', 'frame 1\nframe 2\n')).toBe( + 'old 1\nold 2\x1b[0m\nframe 1\nframe 2\n' + ); + expect(joinScrollbackAndFrame('old 1', 'frame 1')).toBe('old 1\x1b[0m\nframe 1'); + }); + + it('resets SGR at the seam, so the last history row cannot bleed into the frame', () => { + // A remote history row that ends with a background still set (a diff row). + expect(joinScrollbackAndFrame('\x1b[41mRED ROW\n', 'plain\n')).toBe('\x1b[41mRED ROW\x1b[0m\nplain\n'); }); it('no scrollback leaves the frame alone', () => {