diff --git a/src/web/routes/session-routes.ts b/src/web/routes/session-routes.ts index 0a82bdff..67346530 100644 --- a/src/web/routes/session-routes.ts +++ b/src/web/routes/session-routes.ts @@ -3153,8 +3153,16 @@ export function registerSessionRoutes( // `alt=recording`) gets the old visible-frame load over it, tailed, rather // than losing everything above the screen. `alternate_on` cannot tell a // nested tmux from vim or less, so those take this path too while open: - // the load the shell had before tmux-rendered loads existed. - if (isFullReload && query.alt === 'recording' && captureOpts.capturedAlternateOn === true) { + // the load the shell had before tmux-rendered loads existed. Not when the + // history came from the remote tmux (`scrollbackOverride`): a remote pane is + // in the alternate screen only because it hosts that tmux's client, and + // the fallback would throw the remote scrollback away. + if ( + isFullReload && + query.alt === 'recording' && + captureOpts.scrollbackOverride === undefined && + captureOpts.capturedAlternateOn === true + ) { isFullReload = false; captureOpts = {}; liveMuxBuffer = capture(captureOpts); diff --git a/test/routes/session-routes.test.ts b/test/routes/session-routes.test.ts index 400cf30a..5e29290e 100644 --- a/test/routes/session-routes.test.ts +++ b/test/routes/session-routes.test.ts @@ -1349,6 +1349,28 @@ describe('session-routes', () => { } }); + it("keeps a remote session's spliced remote scrollback, and falls back only when that read failed", async () => { + // The remote session's local pane hosts the remote tmux client, so it is + // always in the alternate screen; the history comes from the remote tmux. + const spy = install(true); + const session = harness.ctx._session as unknown as { remote?: unknown }; + session.remote = { hostId: 'h1', label: 'proxx', host: 'proxx.lan', username: 'tim', remotePath: '/home/tim' }; + try { + remoteScrollback.mockResolvedValueOnce('remote line 1\n'); + const spliced = await load(`full=1&tail=${1024 * 1024}&alt=recording&lines=10000`); + expect(spy).toHaveBeenCalledTimes(1); + expect(spliced.source).toBe('mux-full-history'); + + spy.mockClear(); + remoteScrollback.mockResolvedValueOnce(null); + const fallback = await load(`full=1&tail=${1024 * 1024}&alt=recording&lines=10000`); + expect(spy).toHaveBeenCalledTimes(2); + expect(fallback.source).toBe('mux-visible'); + } finally { + session.remote = undefined; + } + }); + it('keeps the full capture without the opt-in (agents, Load full history, scroll pulls)', async () => { const spy = install(true); const data = await load(`full=1&tail=${1024 * 1024}&lines=10000`);