fix(terminal): Claude panes scroll 5 lines per wheel notch
Fullscreen Claude Code scrolls one line per wheel report when its terminal does not answer XTVERSION as xterm.js (behind tmux it never does), and xterm.js sends one report per notch — so a notch moved a single line. spawnRun now sets CLAUDE_CODE_SCROLL_SPEED=5 unless the dashboard's env already sets it; settings.json env and /config Scroll speed still win.
This commit is contained in:
+1
-1
@@ -3804,7 +3804,7 @@ Sent when a notification is created.
|
||||
|
||||
#### /ws-pty/:runId — PTY frames
|
||||
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on workspace load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub workspace shell`, other tools), all receiving the same frames live-synced. **The attach URL carries the client's size** (`/ws-pty/:runId?cols=<n>&rows=<n>`, integers in `[2, 1000]`). A terminal with no size to give — one on a hidden tab, whose box is 0×0 — names none and reports none until it is shown, and its attach is spawned at the window's CURRENT size (`tmux.windowSize`, 80×24 only when tmux cannot answer), so a background tab never shrinks the pane (it used to fit to xterm's 12×6 minimum and resize tmux to that): tmux resizes the window to whichever client attached most recently, so spawning the attach at a default size and correcting it from the first `resize` message a beat later made the pane's program redraw its whole UI at 80 columns — and everything painted at that width stayed in the scrollback, surfacing as torn left-margin fragments the moment the user scrolled back (issue #33). Resize events after that: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and takes the lead whenever the terminal regains focus. **Several browsers on one pane share ONE size** (`server/lib/pty-attach.js`): tmux's `window-size latest` snapped the pane to whoever typed last, so two viewers on screens of different sizes kept resizing it under each other and one saw it cropped. The server names a DRIVER per pane — the first attach, then whichever sends `{"type":"resize","cols","rows","drive":true}` (the client does on focus and on a keystroke, never on a mouse report). Every other attach is a follower: its PTY is held at the driver's size, a follower's own `resize` without `drive` is remembered but not applied, and the server tells it `{"type":"size","cols","rows"}` — the client resizes xterm to that grid and scales it down with CSS to fit, so the browser in use decides the size and the others just shrink. A follower attaches at the driver's size, so joining never resizes the pane. When the driver's socket closes the rest get `{"type":"lead"}` and size themselves again. On Windows every attach is also sent `{"type":"wheel"}`: psmux never turns mouse tracking on for its client, so the browser sends the SGR wheel report (`ESC[<64;1;1M` up, `ESC[<65;1;1M` down) itself while xterm reports no tracking — psmux scrolls, or forwards it to a pane app that tracks the mouse (Claude Code fullscreen). Driver tracking is per server process: a real terminal (`agenthub workspace shell`) or a second dashboard on the same tmux still follows tmux's own `latest` rule. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended — the Workspace takes that frame as the signal to restore the run setup form, since there is nothing left to attach to. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on workspace load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub workspace shell`, other tools), all receiving the same frames live-synced. **The attach URL carries the client's size** (`/ws-pty/:runId?cols=<n>&rows=<n>`, integers in `[2, 1000]`). A terminal with no size to give — one on a hidden tab, whose box is 0×0 — names none and reports none until it is shown, and its attach is spawned at the window's CURRENT size (`tmux.windowSize`, 80×24 only when tmux cannot answer), so a background tab never shrinks the pane (it used to fit to xterm's 12×6 minimum and resize tmux to that): tmux resizes the window to whichever client attached most recently, so spawning the attach at a default size and correcting it from the first `resize` message a beat later made the pane's program redraw its whole UI at 80 columns — and everything painted at that width stayed in the scrollback, surfacing as torn left-margin fragments the moment the user scrolled back (issue #33). Resize events after that: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and takes the lead whenever the terminal regains focus. **Several browsers on one pane share ONE size** (`server/lib/pty-attach.js`): tmux's `window-size latest` snapped the pane to whoever typed last, so two viewers on screens of different sizes kept resizing it under each other and one saw it cropped. The server names a DRIVER per pane — the first attach, then whichever sends `{"type":"resize","cols","rows","drive":true}` (the client does on focus and on a keystroke, never on a mouse report). Every other attach is a follower: its PTY is held at the driver's size, a follower's own `resize` without `drive` is remembered but not applied, and the server tells it `{"type":"size","cols","rows"}` — the client resizes xterm to that grid and scales it down with CSS to fit, so the browser in use decides the size and the others just shrink. A follower attaches at the driver's size, so joining never resizes the pane. When the driver's socket closes the rest get `{"type":"lead"}` and size themselves again. On Windows every attach is also sent `{"type":"wheel"}`: psmux never turns mouse tracking on for its client, so the browser sends the SGR wheel report (`ESC[<64;1;1M` up, `ESC[<65;1;1M` down) itself while xterm reports no tracking — psmux scrolls, or forwards it to a pane app that tracks the mouse (Claude Code fullscreen). Fullscreen Claude Code scrolls ONE line per wheel report when it cannot tell its terminal is xterm.js (behind tmux it never can), and xterm.js sends one report per notch, so `spawnRun` gives every Claude pane `CLAUDE_CODE_SCROLL_SPEED=5` unless the dashboard's own env sets it; the user's `settings.json` `env` or `/config` → Scroll speed still wins. Driver tracking is per server process: a real terminal (`agenthub workspace shell`) or a second dashboard on the same tmux still follows tmux's own `latest` rule. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended — the Workspace takes that frame as the signal to restore the run setup form, since there is nothing left to attach to. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
|
||||
#### cc_config_changed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user