mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(terminal): keep the geometry replay to the pass that can converge
Three follow-ups to the source gate, each one measured rather than reasoned. A pane already drawing at the size the client just requested is left alone. The replay runs at `dimsAfterLoad`, so it can only change what is on screen if the pane was drawing at some other size; when the reported geometry already IS that size, the second pass captures the identical frame and pays a full reload to do it, including a visible re-flash, a dropped and reopened WebSocket and a deleted xterm snapshot. That equality is the signature of a clamp rather than a race: `getTerminalDimensions()` floors at 40x10 while `fitAddon.fit()` does not, so a terminal narrower than 40 columns or shorter than 10 rows reports a pane permanently bigger than itself and replayed on every tab switch without ever converging. A race never produces the equality, since its premise is that the pane was still at the size it was asked to leave. The declined-resize case does not produce it either, so that one still costs the single capped attempt and needs the pane-ownership question this does not touch. The full-history re-arm is unreachable and now says so. A pass that consumed the flag sent `full=1`, and the route answers `full=1` with `mux-full-history` or `history`, never `mux-visible`, so the source gate already rules out every such pass. The line stays for the invariant, but its comment no longer reads as if a page load retries, and the suite pins that it does not. The response no longer reports geometry for a body that carries no capture. The full-history path writes `capturedGeometry` from the cursor query and then returns '' for a pane holding nothing visible, which drops the source to `history` with the geometry already recorded: a `full=1` request whose capture reported 100x50 and returned nothing answered `source: "history"` with both fields set. Nothing acted on it, because the client ignores geometry on any other source, but the field said a frame had been drawn at a size when none had. The browser stub now derives `source` from the request the way the route does, rather than answering `full=1` with `mux-visible`, which the route cannot produce. Each case reaches a visible-frame response the way production does, by not being the first select of the page. Three cases pin the new behaviour and each fails without its guard: the clamp case sees two fetches instead of one, the scope case and the full-history case both see a replay the gate forbids, and the width case sees one fetch instead of two. The changeset now describes the change from 1.29.x rather than the difference between the two commits on this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5cfb98fb8b
commit
e0d4477edc
@@ -6572,6 +6572,27 @@ class CodemanApp {
|
||||
framePositionsRowsAbsolutely &&
|
||||
Number.isFinite(data.captureCols) &&
|
||||
data.captureCols > (this.terminal?.cols || 0);
|
||||
// The retry replays at `dimsAfterLoad`, so it can only change what is on
|
||||
// screen if the pane was drawing at some OTHER size. When the reported
|
||||
// geometry already IS that size, the second pass captures the identical
|
||||
// frame and pays a full reload to do it: another fetch, another
|
||||
// `_resetTerminalForReplay()` and chunked rewrite (a visible re-flash),
|
||||
// and, because it goes through `forceReload`, a dropped and reopened
|
||||
// WebSocket plus a deleted xterm snapshot.
|
||||
//
|
||||
// That equality is the signature of a CLAMP rather than a race.
|
||||
// `getTerminalDimensions()` floors at 40x10 while `fitAddon.fit()` does
|
||||
// not, so a terminal narrower than 40 columns or shorter than 10 rows
|
||||
// reports a pane permanently bigger than itself, and every select would
|
||||
// retry without ever converging. A race never produces this equality: its
|
||||
// whole premise is that the pane was still at the size we asked it to
|
||||
// leave. The other non-converging case, `Session.resize` declining a
|
||||
// small viewport while a desktop claim is live, does not produce it
|
||||
// either — that pane sits at the DESKTOP's size — so it still costs the
|
||||
// one capped attempt, and stopping it needs the pane-ownership policy
|
||||
// this does not touch.
|
||||
const captureMatchesRequestedSize =
|
||||
!!dimsAfterLoad && data.captureCols === dimsAfterLoad.cols && data.captureRows === dimsAfterLoad.rows;
|
||||
|
||||
// Defer secondary panel updates so they don't block the main thread
|
||||
// after terminal content is already visible.
|
||||
@@ -6680,6 +6701,7 @@ class CodemanApp {
|
||||
// trade replays forever.
|
||||
if (
|
||||
(sizeMovedUnderLoad || capturedTallerThanTerminal || capturedWiderThanTerminal) &&
|
||||
!captureMatchesRequestedSize &&
|
||||
!options?.resizeRetry &&
|
||||
!this._isStaleSelect(selectGen)
|
||||
) {
|
||||
@@ -6693,6 +6715,14 @@ class CodemanApp {
|
||||
// that took the bounded tail must retry on the tail too: clearing the
|
||||
// flag unconditionally would UPGRADE a tab switch into a fresh
|
||||
// multi-megabyte scrollback capture it never asked for.
|
||||
//
|
||||
// UNREACHABLE as written, and kept for the invariant rather than the
|
||||
// branch. A `useFullHistory` pass sends `full=1`, and the route answers
|
||||
// `full=1` with `mux-full-history` or `history`, never `mux-visible`
|
||||
// (see the source ladder in session-routes.ts), so the gate above
|
||||
// already rules out every pass that consumed the flag. Do not read this
|
||||
// line as evidence that a page load retries: it does not, and the test
|
||||
// suite pins that it does not.
|
||||
if (useFullHistory) this._fullHistoryLoaded.delete(sessionId);
|
||||
await this.selectSession(sessionId, { auto: true, forceReload: true, resizeRetry: true });
|
||||
}
|
||||
|
||||
@@ -2793,15 +2793,20 @@ export function registerSessionRoutes(
|
||||
// rows than this overwrites its last line with the overflow and loses
|
||||
// the rows underneath. The client compares these against its own size.
|
||||
//
|
||||
// BOTH FIELDS ARE ABSENT when the capture reported no geometry, and that
|
||||
// is the honest answer rather than a gap to paper over: the cursor query
|
||||
// is what produces the absolute addressing in the first place, so a
|
||||
// capture that lost it returned a raw frame with no row positioning in
|
||||
// it, and a byte-history response was never positioned at all. Reporting
|
||||
// the session's own PTY size here would name a geometry no frame was
|
||||
// built for and invite the client to repair damage that does not exist.
|
||||
captureCols: captureOpts.capturedGeometry?.cols,
|
||||
captureRows: captureOpts.capturedGeometry?.rows,
|
||||
// BOTH FIELDS ARE ABSENT unless this response really carries a capture,
|
||||
// and that is the honest answer rather than a gap to paper over. Two
|
||||
// separate things can leave a frame unpositioned. The cursor query is
|
||||
// what produces the absolute addressing in the first place, so a capture
|
||||
// that lost it returned a raw frame with no row positioning in it. And a
|
||||
// capture can report geometry and STILL hand back nothing: the
|
||||
// full-history path returns '' for a pane holding nothing visible, which
|
||||
// drops `source` to `history` while `capturedGeometry` is already
|
||||
// written, so the geometry has to be suppressed HERE rather than trusted
|
||||
// to be missing. Naming a size for a body that is the byte stream would
|
||||
// describe a frame that was never drawn and invite the client to repair
|
||||
// damage that does not exist.
|
||||
captureCols: hasLiveMuxBuffer ? captureOpts.capturedGeometry?.cols : undefined,
|
||||
captureRows: hasLiveMuxBuffer ? captureOpts.capturedGeometry?.rows : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user