Merge pull request #331 from dignfei/fix/shell-history-performance

fix(terminal): bound shell history replay
This commit is contained in:
Ark0N
2026-08-21 20:23:17 +02:00
committed by GitHub
16 changed files with 414 additions and 138 deletions
+51 -31
View File
@@ -1568,6 +1568,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
private reconnectGuard: Set<string> = new Set();
private trueColorConfigured = false;
/** tmux 3.7+ can resize pane history after creation; older releases cannot. */
private liveHistoryResizeSupported: boolean | null = null;
constructor() {
super();
@@ -1586,6 +1588,26 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
return tmuxCommand(this.tmuxSocket);
}
private supportsLiveHistoryResize(): boolean {
if (this.liveHistoryResizeSupported !== null) return this.liveHistoryResizeSupported;
try {
const output = execSync(`${this.tmux()} -V`, {
encoding: 'utf8',
timeout: EXEC_TIMEOUT_MS,
stdio: ['ignore', 'pipe', 'ignore'],
});
const match = output.match(/(?:^|\D)(\d+)\.(\d+)/);
const major = match ? Number(match[1]) : 0;
const minor = match ? Number(match[2]) : 0;
this.liveHistoryResizeSupported = major > 3 || (major === 3 && minor >= 7);
} catch {
// Unknown versions take the legacy path required by tmux <3.7.
this.liveHistoryResizeSupported = false;
}
return this.liveHistoryResizeSupported;
}
// Load saved sessions from disk (NEVER called in test mode)
private loadSessions(): void {
if (IS_TEST_MODE) return;
@@ -1926,7 +1948,16 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// launched in TMUX_LAUNCH_CWD (/tmp) rather than the real workingDir: a FUSE/rclone
// mount that isn't ready yet makes `getcwd` fail and breaks the spawn (see #110). The
// pane cd's into workingDir below via respawn-pane.
execSync(`${this.tmux()} new-session -ds "${muxName}" -c ${TMUX_LAUNCH_CWD}`, {
// tmux <3.7 allocates history only at pane creation, so its global default
// must be set immediately BEFORE new-session. tmux 3.7+ can resize a pane
// after creation; target only the new session there because changing the
// global option can resize (and when lowered, trim) unrelated live panes.
const safeHistoryLimit =
Number.isSafeInteger(historyLimit) && historyLimit > 0 ? Math.trunc(historyLimit) : DEFAULT_TMUX_HISTORY_LIMIT;
const createSessionCommand = this.supportsLiveHistoryResize()
? `${this.tmux()} new-session -ds "${muxName}" -c ${TMUX_LAUNCH_CWD} \\; set-option -t "${muxName}" history-limit ${safeHistoryLimit}`
: `${this.tmux()} set-option -g history-limit ${safeHistoryLimit} \\; new-session -ds "${muxName}" -c ${TMUX_LAUNCH_CWD} \\; set-option -t "${muxName}" history-limit ${safeHistoryLimit}`;
execSync(createSessionCommand, {
cwd: TMUX_LAUNCH_CWD,
timeout: EXEC_TIMEOUT_MS,
stdio: 'ignore',
@@ -1991,16 +2022,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
.catch(() => {
/* Already set globally as fallback */
}),
// Raise tmux scrollback from its 2000-line default so re-attach preserves
// more context. Intentionally exceeds the xterm-side DEFAULT_SCROLLBACK (50k
// in constants.js), which stays lower to protect browser/mobile memory.
execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit ${historyLimit}`, {
timeout: EXEC_TIMEOUT_MS,
})
.then(() => {})
.catch(() => {
/* Non-critical — falls back to tmux default */
}),
];
// Enable 24-bit true color passthrough — server-wide, set once per lifetime
@@ -2124,7 +2145,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
resumeSessionId,
envOverrides,
effort,
historyLimit = DEFAULT_TMUX_HISTORY_LIMIT,
remote,
docker,
name,
@@ -2135,16 +2155,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
if (!isValidMuxName(muxName) || !isValidPath(workingDir)) return null;
// Re-apply the configured tmux history-limit after respawn (kept in sync
// with the live setting via setHistoryLimit()).
if (!IS_TEST_MODE) {
await execAsync(`${this.tmux()} set-option -t ${shellescape(muxName)} history-limit ${historyLimit}`, {
timeout: EXEC_TIMEOUT_MS,
}).catch(() => {
/* Non-critical — keeps existing tmux history-limit */
});
}
// Resolve CLI binary directory based on mode
const { pathExport } = this.buildPathExport(mode);
@@ -3003,9 +3013,9 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
}
/**
* Apply a tmux history-limit to all tracked sessions (e.g. when the user
* changes the terminal-history setting). Invalid limits fall back to the
* default. Best-effort per session.
* Apply a tmux history limit. tmux 3.7+ safely targets tracked live sessions;
* older releases can only change the global default for future panes. Invalid
* limits fall back to the default.
*/
async setHistoryLimit(limit: number): Promise<void> {
const safeLimit = Number.isSafeInteger(limit) && limit > 0 ? Math.trunc(limit) : DEFAULT_TMUX_HISTORY_LIMIT;
@@ -3014,12 +3024,22 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
return;
}
const updates = Array.from(this.sessions.values()).map((session) =>
execAsync(`${this.tmux()} set-option -t ${shellescape(session.muxName)} history-limit ${safeLimit}`, {
timeout: EXEC_TIMEOUT_MS,
})
);
await Promise.allSettled(updates);
if (this.supportsLiveHistoryResize()) {
const updates = Array.from(this.sessions.values()).map((session) =>
execAsync(`${this.tmux()} set-option -t ${shellescape(session.muxName)} history-limit ${safeLimit}`, {
timeout: EXEC_TIMEOUT_MS,
})
);
await Promise.allSettled(updates);
return;
}
await execAsync(`${this.tmux()} set-option -g history-limit ${safeLimit}`, {
timeout: EXEC_TIMEOUT_MS,
}).catch(() => {
// No tmux server yet is fine: legacy createSession sets the same default
// immediately before it creates the first pane.
});
}
/**