Skip to content

fix(terminal): partial-history notice only when there is history to load - #583

Merged
Ark0N merged 1 commit into
masterfrom
fix/history-notice-lazy
Oct 13, 2026
Merged

Ark0N merged 1 commit into
masterfrom
fix/history-notice-lazy

Conversation

@Ark0N

@Ark0N Ark0N commented Oct 10, 2026

Copy link
Copy Markdown
Owner

When a terminal replay is cut short, the web UI shows a bar over the top of the terminal: "Showing the most recent 1.0 MB of this session. 4.8 MB more may still be retained." with a Load full history button (#258). This PR makes that bar appear only when it means something and only when you are looking for history. Before, it showed on almost every tab switch and could not be acted on for fullscreen Claude sessions.

What was wrong

  • It showed on almost every tab switch. Every tab switch after a session's first load replays a 1 MiB tail of the session's output stream. Any session that has run for a while has more than that, so the replay is "truncated" and the bar appears. Its × only lasted until the next switch.
  • For fullscreen Claude it promised history that does not exist. truncated measures the server's raw byte stream. Load full history asks tmux for its scrollback instead. A fullscreen Claude pane lives in the alternate screen and tmux keeps 0 lines of scrollback for it (Claude keeps the conversation itself). So the "4.8 MB more" was old screen redraws, and the button could only end in the existing downgrade refusal ("Earlier output is no longer kept").

What this PR changes

  1. Server reports what a full pull can return. capturePaneBuffer already runs one display-message for the cursor and pane size. That same call now also reads #{history_size} (no extra tmux call). GET /api/v1/sessions/:id/terminal returns it as paneHistoryLines, the rows tmux holds above the visible frame. The field is absent when the body is not a pane capture. This is an additive response field.
  2. No notice when there is nothing to load. paneHistoryLines === 0 hides it in every state. A positive count replaces the misleading byte figure: "99,971 lines of scrollback are retained." Without the field (an older server, or a byte-history fallback) the old wording is kept.
  3. The notice is lazy.
    • It appears once a wheel or touch gesture reaches the top of the browser buffer, after the history pull that gesture already starts has finished. So it describes what the pull left, not the state it is about to replace.
    • It leaves when a downward gesture is back at live output, or on a tab switch.
    • The × now sticks for that session until the page reloads.
  4. formatHistoryBytes no longer prints "1024 KB" for a tail cut just under 1 MiB.

Not changed: what any load fetches or replays, the downgrade guard, the tile grid, and shell pull sizes.

Evidence

Measured on a live instance with 11 fullscreen Claude sessions:

  • Every pane: alternate_on=1, history_size=0.
  • Tab-switch tails cut 1.2 to 6.2 MB streams down to 1 MiB.
  • full=1 returned 0.9 to 5.6 KB, which is the visible frame only.
  • Replaying one tail into a headless xterm gave 450 non-blank scrollback rows, of which only 44 were different (one row appeared 33 times).

End to end in Chromium on an isolated instance, with trusted wheel events:

  • Shell with ~100k lines of tmux history: hidden after the tab load, shown at the top with the line count and the button, hidden again at the bottom, shown again at the top, and gone for good after ×.
  • Pane set up like fullscreen Claude (alternate screen, history cleared, 4.7 MB stream, full=1 = 155 bytes): never shown, even at the top.

Why history_size and not alternate_on

A shell running less is also in the alternate screen, but tmux still keeps its whole history (measured 99,972 rows), and full=1 returns all of it. history_size is exactly what the full pull can add, so the decision keys on that.

Reviewing the diff

About 330 of the 561 added lines are tests and about 20 are docs. The logic is roughly 110 lines:

  • src/tmux-manager.ts: queryPaneCursor parses an optional fifth field, validated on its own so a bad value never voids the caret; capturePaneBuffer copies it to opts.capturedHistoryLines.
  • src/web/routes/session-routes.ts: one response field.
  • src/web/public/constants.js: computeHistoryTruncationNotice gating and wording.
  • src/web/public/app.js: banner render gate, sticky ×, _setHistoryNoticeRevealed, and the reset in selectSession.
  • src/web/public/terminal-ui.js: _maybeLoadMoreHistoryOnScroll reveals at the top and retires at the bottom.

New test: test/history-notice-reveal.test.ts runs the real app.js / terminal-ui.js / constants.js methods in a vm against a stub DOM. Removing the reveal gate makes 5 of its tests fail.

Risk

  • Only scroll gestures reveal the notice (wheel, touch, smooth-scroll frames). These are the same hooks that already trigger the history pull. Dragging xterm's scrollbar to the top does not reveal it, just as it does not trigger the pull today.
  • A shell user who relied on the bar showing at load now has to scroll up to see Load full history. That is the intended trade.

Testing

  • npm test (the CI gate): 10,376 passed.
  • typecheck, lint, format:check and check:frontend-syntax clean.

The "Showing the most recent 1.0 MB of this session. 4.8 MB more may still
be retained." bar covered the top of the terminal on nearly every tab switch,
and for fullscreen claude panes it could never be acted on: `truncated`
measures the server's byte stream, while Load full history asks tmux, which
holds no scrollback for a pane that lives in the alternate screen.

- The capture reads `#{history_size}` in the cursor query it already makes
  and the terminal route returns it as `paneHistoryLines`.
- A reported 0 hides the notice; a positive count replaces the byte gap in
  the message.
- The notice is lazy: it appears once a wheel/touch gesture reaches the top
  of the browser buffer (after the pull that gesture starts), leaves when
  the user scrolls back to live output or switches tabs, and its dismissal
  sticks per session until reload.
- formatHistoryBytes no longer prints "1024 KB" just under 1 MiB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N
Ark0N merged commit f2d5af2 into master Oct 13, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 13, 2026
#583 merge-time fix)

_smoothScrollBy and the touch paths call _maybeLoadMoreHistoryOnScroll on
every frame. Once the viewport sat at row 0, the next frame hit the real
pull's in-flight early return, so its reveal ran at once and the notice
showed the pre-pull tail state for the whole pull. A later frame now leaves
the reveal to the call that started the pull.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 13, 2026
…read (#583 merge-time fix)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 13, 2026
…ered history, not the byte recording

Conflicts resolved against #583 and #588:
- One history-depth field: #583's `historyLines` / `capturedHistoryLines`
  (reported as `paneHistoryLines`) is kept, and #601's lines cut now reads it
  instead of a second `capturedHistorySize`. #601's `alternateOn` /
  `capturedAlternateOn` joins it, and the one display-message format string
  ends `#{history_size} #{alternate_on}`.
- selectSession keeps #588's full=1 switch for panes with no scrollback, gated
  off remote and Docker sessions, and #601's SHELL_LOAD_QUERY for shells.
- computeHistoryTruncationNotice names a line count when paneHistoryLines is
  known (#583) and otherwise uses #601's remainder wording.
- docs: api-reference lists both historyLinesCut and paneHistoryLines; the
  notice invariant keys on history_size while the shell's automatic load is
  the one place alternate_on decides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 13, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants