Repository navigation
fix(remote): load a remote session's scrollback from the remote tmux - #602
Conversation
A remote SSH session's local pane runs `ssh -t … tmux new-session -A` (or
`attach`), so the local tmux only sees the remote tmux client's full-screen
repaints and keeps a fraction of the history: measured 48 of 279 lines while
output streamed, none after a reattach. The remote pane, where the shell
runs, keeps all of it.
A full-history load (`GET /api/sessions/:id/terminal?full=1`) of a remote
session now reads the scrollback rows from the remote pane in one ssh round
trip (`#{history_size}` + `capture-pane -S -<N> -E -1`, exact `=name:`
target, newest bytes kept by a remote `tail -c`), and the local capture
reads only its visible frame (`-S 0`), spliced below them
(`PaneCaptureOptions.scrollbackOverride`). The local frame and caret are
kept because the live stream keeps drawing into the local pane.
Fail-soft and bounded: connection options from buildSshConnectionArgs(),
the shared remote-ssh limiter (a slot waited for longer than 1.5 s is given
up), a 5 s timeout, and a 30 s per-host back-off after any failure, which
keeps the local capture as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks a lot for this, @timkjr. It fixes the near-empty scrollback on remote SSH sessions by reading the history from the remote tmux pane and splicing it above the local frame, and the write-up with the measurements made it easy to review. I ran it against real tmux (a 'remote' tmux server and a local pane attached to it, then your script and One thing needs fixing before merge, plus a few smaller ones:
Thanks also for flagging the interaction with #601. I'll settle the merge order between the two, and whichever lands second gets the small adjustment. Once item 1 is in (2 to 5 can come in the same push), this is ready to merge. |
…f only on transport failures Review follow-ups for the remote scrollback read. The 1.5 s slot-wait bound was checked inside the task, so it only fired once runWithRemoteSshLimit() had already handed over a slot, after everything queued ahead of it. runWithRemoteSshLimit() now takes `maxWaitMs`: a waiter that times out leaves the queue, rejects with RemoteSshSlotTimeoutError, never runs and never touches `active`. The history read uses it, and a slot timeout does not back the host off. Only a transport failure backs the host off now (ssh exit 255, a timeout, a spawn error, output that is not the script's), via classifyRemoteAliveExit(). A remote command failing is about that session only. A live run against a real host showed a missing session comes back as exit 0 with empty markers, because the `| tail -c` pipe takes tail's exit status, so markers that arrived without a size are also per-session. fetchRemoteScrollback() takes an optional runner as a test seam; tests now cover the bounded wait, each back-off class, its expiry, and the masked missing-session case. A source pin covers the `-S 0` capture branch. The two seams that can drop rows at the join (rows that scroll during the round trip, a taller shared client) are documented in docs/remote-sessions.md, and the limiter's header and the remote-ssh invariants now name the history read as an ssh consumer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for running it against real tmux. The follow-ups are in 136f115, on top of the original commit.
The #601 interaction is in my reply there: with both in, #601's alternate-screen fallback would discard this splice for remote shells, so whichever lands second needs to skip it when the remote override was used. |
…ge-time fix) joinScrollbackAndFrame joins two separate `capture-pane -e` outputs. The remote history's last row can end with attributes still active (a diff row's background, dim tool output), while the local frame is encoded relative to the default state, so its first plain rows inherited them in the browser. A `\x1b[0m` now ends the scrollback before the frame. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…602 integration) A remote session's local pane hosts the remote tmux client, so it is always in the alternate screen. #601's alt=recording fallback therefore threw away the scrollback #602 had just spliced in from the remote tmux on every remote shell load. The fallback now applies only when no remote scrollback was read (the read failed, or the session is not remote). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in a remote history load Conflict resolved in tmux-manager.ts: #609's screen-aware joinScrollbackAndFrame keeps #602's merge-time SGR reset before the frame, and #609's join tests expect it. The #601/#602 route test now mocks the remote read in #609's { scrollback, screen } shape. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tegration) A remote session's local pane hosts the remote tmux client in the alternate screen, so its history_size is always 0. #583 read that as "nothing to load" and hid the partial-history notice, but with #602 a full=1 load reads the real scrollback from the remote tmux. The route now leaves the field out for remote sessions, so their notice keeps the byte-based wording and its Load full history button. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-roll video uploads Conflicts resolved against #605 (and #602's test mock): - CLAUDE.md Prompt uploads: #606's rewrite is kept and #605's bounded workspace probe sentence follows the remote (SSH) refusal. - test/routes/session-routes.test.ts: the remote-pane-history and bounded-path-probe mocks are kept beside #606's _resetPasteRateBuckets import, and both PRs' paste-image tests are kept. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks @timkjr, shipped in 1.41.1. One fix at merge: the remote history and the local frame come from two separate |
A remote SSH session's local pane runs
ssh -t … tmux new-session -A(orattach), so the local tmux only sees the remote tmux client's full-screen repaints and keeps a fraction of the history: measured 48 of 279 lines while output streamed, none after a reattach. The remote pane, where the shell runs, keeps all of it.GET /api/sessions/:id/terminal?full=1) of a remote session now reads the scrollback rows from the remote pane in one ssh round trip (#{history_size}+capture-pane -S -<N> -E -1, exact=name:target, newest bytes kept by a remotetail -c).-S 0), spliced below them (PaneCaptureOptions.scrollbackOverride). The local frame and caret are kept because the live stream keeps drawing into the local pane.buildSshConnectionArgs(), the shared remote-ssh limiter (a slot waited for longer than 1.5 s is given up), a 5 s timeout, and a 30 s per-host back-off after any failure, which falls back to the local capture as before.docs/remote-sessions.mdupdated.Testing: new
test/remote-pane-history.test.tsand route tests; typecheck, lint, frontend syntax, format check and the full suite green on this branch. Live against a remote host on my own deployment: afull=1load takes ~170–200 ms (the ssh round trip) with no fallback warnings.Known interaction with #601: if both land, the
'tail'truncation flag added there compares against the LOCAL pane's#{history_size}. For a remote shell that no longer describes the spliced remote history, so the banner may not offer Load full history for a remote shell with more than 10,000 remote lines. Not fixed in either PR; happy to follow up once the order is settled.