mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
fix(terminal): stop the scroll-to-top re-pull from deleting history, page the CLI when local scrollback is hollow (#205)
The 1.12.0 retest on #205 reported it still broken in two shapes: a wheel that did nothing at all on Firefox/macOS (while Fn+Up paged back through intact text), and iPhone history that went back a little, repeated blocks and got worse the further up it went. Both come from a Claude pane's LOCAL buffer being hollow: tmux keeps no history for a repaint-mode pane (history_size 0), so xterm holds only replayed repaint frames. 1. The scroll-to-top full=1 re-pull now refuses a DOWNGRADE. It resets the terminal and rewrites it from the capture, which is a win when tmux holds more than the browser, but for a repaint-mode pane that capture is roughly ONE frame and the rewrite deleted history mid-scroll. Measured A/B on a live pane, same gesture: guard off collapses 341 rows to 42, guard on preserves all 341. _replayWouldShrinkBuffer() estimates the capture's rendered rows (escapes stripped, capture-pane -J re-wrapping accounted for) and skips the rewrite when it is more than one screen short; a refused session's cooldown goes from 4s to 60s so a hollow pane stops re-fetching megabytes. 2. A false forwarding gate on a Claude session no longer means a dead gesture. Under a triple guard (claude mode, gate false, baseY 0), wheel and touch travel becomes coalesced PageUp/PageDown through the same 40ms queue as the SGR reports, at half a screen of travel per page key. Shift is excluded: it keeps meaning "local scrollback". 3. getClaudeCliVersion() no longer caches FAILURE. It stored null on any exception and guarded on !== undefined, so one timed-out or PATH-starved probe at the first Claude session start disabled wheel-forwarding for every Claude session until the server restarted, which fits a report of breakage on phone, tablet and laptop at once. Success is still cached for the process lifetime; failures retry with a 1/2/4 up to 15min backoff, and the policy is a pure function so the semantics are testable without spawning claude. 4. The terminalWheelLocalScrollback footgun is handled by pairing rather than scoping: the setting keeps meaning exactly what it says, and fix 2 catches the case where "local" is empty. The App Settings tooltip now says to leave it off for Claude/Codex sessions. 5. _logScrollRouting() prints one line per session per distinct decision: forward-sgr / page-keys / local-scrollback / repull-refused-downgrade, with mode, cliVersion, the opt-out state, mouse tracking and local scrollback depth. #205 ran two rounds of remote guesswork over questions that line answers directly. Verified end to end against a real isolated instance (own data dir and tmux socket) with real wheel events: forwarding still sends SGR reports, the opt-out now sends real PageUp/PageDown where the wheel was dead, a tab-switch collapse (401 rows to 44) is still fully recovered by the re-pull (back to 401), and a seeded 341-row Claude buffer survives the same gesture that destroys it with the guard disabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+19
-1
@@ -519,6 +519,10 @@ class CodemanApp {
|
||||
// Cooldown per session for the scroll-to-top "load more history" re-pull.
|
||||
this._fullHistoryRepullAt = new Map(); // Map<sessionId, timestamp>
|
||||
this._fullHistoryRepullInFlight = false;
|
||||
// Sessions whose last re-pull came back THINNER than the live buffer (a
|
||||
// repaint-mode CLI pane, where tmux keeps no history of its own). The pull is
|
||||
// refused for those and retried far more slowly — see _maybeRefetchFullHistory.
|
||||
this._fullHistoryRepullUseless = new Set();
|
||||
this.terminalLoadStates = new Map(); // Map<sessionId, { generation, phase }>
|
||||
this.respawnStatus = {};
|
||||
this.respawnTimers = {}; // Track timed respawn timers
|
||||
@@ -4121,6 +4125,13 @@ class CodemanApp {
|
||||
* On demand rather than automatic because that capture is unbounded-ish work: at
|
||||
* the default history limit it can be megabytes, which is fine to pay when the
|
||||
* user is explicitly reaching for history and not fine on every tab switch.
|
||||
*
|
||||
* NEVER a downgrade: for a repaint-mode CLI pane tmux keeps no history of its
|
||||
* own, so the capture can be THINNER than what xterm already holds and the
|
||||
* reset+rewrite below would delete history mid-scroll. `_replayWouldShrinkBuffer`
|
||||
* (terminal-ui.js) is the guard, and a session that produced one useless re-pull
|
||||
* gets a much longer cooldown so a hollow pane stops re-fetching megabytes on
|
||||
* every scroll-up (issue #205, round 2).
|
||||
*/
|
||||
async _maybeRefetchFullHistory() {
|
||||
const sessionId = this.activeSessionId;
|
||||
@@ -4129,7 +4140,8 @@ class CodemanApp {
|
||||
const now = Date.now();
|
||||
// Momentum scrolling fires this dozens of times per flick, and a burst of new
|
||||
// output is the normal reason to want a re-pull, so cooldown rather than latch.
|
||||
if (now - (this._fullHistoryRepullAt.get(sessionId) || 0) < 4000) return;
|
||||
const cooldown = this._fullHistoryRepullUseless?.has(sessionId) ? 60000 : 4000;
|
||||
if (now - (this._fullHistoryRepullAt.get(sessionId) || 0) < cooldown) return;
|
||||
this._fullHistoryRepullAt.set(sessionId, now);
|
||||
this._fullHistoryRepullInFlight = true;
|
||||
try {
|
||||
@@ -4138,6 +4150,12 @@ class CodemanApp {
|
||||
// Bail on a tab switch mid-fetch: writing here would paint another session's
|
||||
// history into the terminal the user is now looking at.
|
||||
if (!buffer || this.activeSessionId !== sessionId) return;
|
||||
if (this._replayWouldShrinkBuffer(buffer)) {
|
||||
(this._fullHistoryRepullUseless ||= new Set()).add(sessionId);
|
||||
this._logScrollRouting?.('repull-refused-downgrade');
|
||||
return;
|
||||
}
|
||||
this._fullHistoryRepullUseless?.delete(sessionId);
|
||||
const rowsBefore = this.terminal.buffer.active.length;
|
||||
this._resetTerminalForReplay();
|
||||
await this.chunkedTerminalWrite(buffer, TERMINAL_CHUNK_SIZE, sessionId);
|
||||
|
||||
Reference in New Issue
Block a user