mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 23:19:43 +02:00
fix(terminal): the PTY and the browser terminal must never disagree about size
Issue #464, "text gets muffled sometimes, in both TUI default and fullscreen". The screenshot is not a dropped frame or a frozen renderer — it is arithmetic. Claude Code's TUI wraps its frame at the width the PTY reported and erases the previous frame by walking the cursor up the rows it believes that frame took. A browser terminal of a different width makes each logical line occupy more physical rows than Ink counted, so `eraseLines(n)` clears too few and the new frame paints over rows nothing erased: doubled lines, and short tool summaries sitting inside longer prose rows with the prose's tail still visible. Reproduced against this repo's own xterm before changing anything — a 120-column PTY against a 62-column terminal renders every wrapped line twice. `test/ terminal-pty-geometry.test.ts` pins that, and pins the clean render at matching widths beside it, so the assertion cannot be satisfied by code that fixes nothing. Four ways the two drifted apart, none of them observable from either end: 1. `fitAddon.fit()` resizes xterm to `proposeDimensions()` RAW while every server-facing path reported those floored at 40x10. Measured in Chrome at 430px: font size 44 proposed 13 columns, the server was told 40, and xterm stayed at 13. Three call sites each did their own fit-then-floor, and two re-read the proposal after the fit — `_shrinkPaddingToFit()` runs exactly there, so the container had moved. 2. `throttledResize` (keyboard up) and `sendResize` (session detached into its own window) reflowed locally and withheld only the SIGWINCH. That is the one combination that cannot be right: a reflow nothing is rendering for buys nothing and costs correctness. Both now withhold everything, and the keyboard's settle timer still sends the one resize that stops the PTY going stale. 3. `setFontSize`/`setFontFamily`/`setFontWeight` move the cell size — a geometry change — and told the server nothing at all, so raising the font on a phone left the CLI wrapping at the old column count. 4. `Session.resize` DECLINES a small-viewport request while a desktop connection holds an active sizing claim, and said nothing, because resize was write-only. `syncTerminalGeometry()` is now the one function that may change the terminal's size: it fits, floors and applies as a single step, so the numbers xterm holds are the numbers the server is told. A test sweeps every module for a bare `fit()` on the main terminal, and finds exactly one — the owner's own. For (4) the client cannot win, so it is told the truth instead: both transports answer a resize with `session.ptyCols`/`ptyRows` (`{"t":"zc"}` on the socket, the body of the resize POST) and `_onPtyGeometryReport` adopts them. A terminal that keeps a shape the PTY refused does not render "too narrow", it renders garbled. Adopting can leave the pane wider than the screen and the container is `overflow: hidden`, so `.pty-oversized` grants horizontal reach for exactly as long as the mismatch lasts: correct-and-reachable beats correct-and-clipped beats garbled. That rule sets both overflow axes and its own `touch-action` because mobile.css loads later and sets `.terminal-container { overflow: visible; touch-action: none }` — a bare `overflow-x` would leave overflow-y computing to `auto` and hand the browser a vertical scroll container the terminal's touch handler knows nothing about. Verified in Chrome at 430px against a live server, with a desktop client holding the claim: the phone adopts 198x43, gets `overflow-x: auto` / `overflow-y: hidden` / `touch-action: pan-x`, 758px of reach to the right, and keeps its own vertical scrolling. The pre-fix build was measured in the same harness for the control. Two things this deliberately does not do. It does not change who owns the pane size — the desktop still wins, and `_startMobileResizeRetry` still takes it back once that goes idle. And `throttledResize` still holds the PTY's shape for the whole keyboard animation rather than sending a SIGWINCH per step; that decision predates this and was not re-tested here. Also in this commit, Ark0N's third-pass review items on #431: - The response viewer's byte-buffer fallback and `_onSessionClearTerminal` both used the no-param `/terminal` form, capped only by `terminalBufferMaxBytes` (32MB) — the largest body the frontend asks for anywhere. One carried no deadline at all and the other got the 15s tail budget. Both now take the full-history budget. - A `?full=1` capture that outruns its deadline falls back to the bounded tail. The pane is blanked before that fetch, so an abort used to leave a black rectangle, discard the queued live output and never reach `_connectWs`. A failed load now still opens the socket, says one dim line where the content would have been, and clears the tab's spinner — which nothing did, so a failed select left `aria-busy="true"` set forever. - `_wsOutputGapSession` is cleared at the repaint that settles it, not in a `finally` that also ran on the catch. A reconcile that threw, or hit the new deadline — the flaky link the marker exists for — dropped the gap with nothing to retry it. `ws.onopen` no longer clears it up front either. - The replay-clear invariant is pinned in the gate, which is the drift this PR exists to fix: `_resetTerminalForReplay` must be a queued write and nothing else, and no module may blank the terminal with a `clear()+reset()` pair. - `DIAG_ENTRY_MAX_CHARS` replaces the hardcoded 300, bound through a local first: `CodemanDiag?.x` still throws a ReferenceError when the identifier was never declared, and that is the one function in the app that must not throw. - panels-ui's two kill-all clears route through the same helper, and the xterm-version guard's comment says "resolved lockfile version" rather than "dependency RANGE", which is what it has pinned since the last round. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
abd39318e6
commit
abf1d1f1ca
@@ -3817,6 +3817,42 @@ body.solo-mode .btn-lifecycle-log {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* A PTY wider than this screen (issue #464). Another device holds the session's
|
||||
sizing claim, so the browser terminal has adopted the PTY's width: the text is
|
||||
rendered CORRECTLY, it simply does not fit. Without horizontal reach the right
|
||||
columns sit behind .terminal-container's overflow:hidden with no gesture that
|
||||
can get to them — correct-but-unreachable is no better than garbled.
|
||||
Present only while the mismatch is; _setPtyOversized() owns the class. */
|
||||
.terminal-container.pty-oversized {
|
||||
/* ⚠️ BOTH axes, explicitly, and touch-action here rather than only on the
|
||||
.touch-device variant below. mobile.css loads after this file and sets
|
||||
`.terminal-container { overflow: visible; touch-action: none }` — a bare
|
||||
`overflow-x` would then leave overflow-y computing to `auto` (CSS promotes
|
||||
a `visible` paired with a non-visible axis), handing the browser a vertical
|
||||
scroll container the terminal's own touch handler does not know about. */
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
/* pan-x ONLY: the terminal's touchmove handler still owns vertical scrolling. */
|
||||
touch-action: pan-x;
|
||||
}
|
||||
/* xterm's own element is width:100% above, so the container would see no
|
||||
overflow to scroll even though .xterm-screen is wider than both. */
|
||||
.terminal-container.pty-oversized .xterm {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
/* The inner elements carry touch-action: none of their own (both here and in
|
||||
mobile.css), so the container's pan-x is not enough on its own. */
|
||||
.touch-device .terminal-container.pty-oversized,
|
||||
.touch-device .terminal-container.pty-oversized .xterm,
|
||||
.touch-device .terminal-container.pty-oversized .xterm-viewport,
|
||||
.touch-device .terminal-container.pty-oversized .xterm-screen,
|
||||
.terminal-container.pty-oversized .xterm,
|
||||
.terminal-container.pty-oversized .xterm-viewport,
|
||||
.terminal-container.pty-oversized .xterm-screen {
|
||||
touch-action: pan-x;
|
||||
}
|
||||
|
||||
/* Touch devices: prevent browser from claiming the touch gesture before
|
||||
our JS touchmove handler fires. Without this, the browser starts native
|
||||
scrolling during the first few px of finger travel and ignores our
|
||||
|
||||
Reference in New Issue
Block a user