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

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>
This commit is contained in:
Codeman maintainer committed 2026-10-10 05:56:37 +02:00
1 parent 89177651a6
commit e2453fe97b
14 files changed
+561 -15

No files matched your search

+15 -3
View File
@@ -700,6 +700,12 @@ interface PaneCursorGeometry {
rows: number;
cursorX: number;
cursorY: number;
/**
* `#{history_size}`: rows tmux holds ABOVE the visible frame, i.e. what a
* full-history capture can add. Absent when the query did not return it.
* Optional and validated on its own, so a bad value never voids the caret.
*/
historyLines?: number;
}
/**
@@ -716,7 +722,7 @@ export function queryPaneCursor(run: () => string): PaneCursorGeometry | null {
console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
return null;
}
const [cursorX, cursorY, cols, rows] = raw.split(/\s+/).map((value) => parseInt(value, 10));
const [cursorX, cursorY, cols, rows, historyLines] = raw.split(/\s+/).map((value) => parseInt(value, 10));
if (
!Number.isFinite(cursorX) ||
!Number.isFinite(cursorY) ||
@@ -729,7 +735,9 @@ export function queryPaneCursor(run: () => string): PaneCursorGeometry | null {
) {
return null;
}
return { cols, rows, cursorX, cursorY };
const geometry: PaneCursorGeometry = { cols, rows, cursorX, cursorY };
if (Number.isFinite(historyLines) && historyLines >= 0) geometry.historyLines = historyLines;
return geometry;
}
/** SGR attributes, which is all `capture-pane -e` emits. */
@@ -3890,7 +3898,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// to keep when a move follows to put the caret back above them.
const geometry = queryPaneCursor(() =>
execSync(
`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`,
`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height} #{history_size}'`,
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
)
);
@@ -3902,6 +3910,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// geometry is reported there for diagnosis rather than for repair. Only
// the caller can see both sizes, so hand it this one.
if (opts && geometry) opts.capturedGeometry = { cols: geometry.cols, rows: geometry.rows };
// Same query, no extra tmux call: how much scrollback a full-history pull
// could return. A pane in the alternate screen (fullscreen claude) holds
// none, and the partial-history notice must not promise it.
if (opts && geometry?.historyLines !== undefined) opts.capturedHistoryLines = geometry.historyLines;
if (fullHistory) {
// Without geometry there is no cursor move, so fall back to the old trim.