Summary
Terminal output inside a psmux pane is rendered with most cells on some rows cleared, leaving only a sparse set of characters scattered at wide column positions. Other rows on the same screen render fully and correctly. This appears visually similar to #73, but reproduces locally — no SSH involved — so the mangling is not purely an ANSI/SSH-transport issue.
Observed inside Claude Code (React Ink TUI). Not reproducible on demand; exact trigger unknown.
Screenshot
Environment
- Windows 11
- psmux version:
- No SSH; direct local session
- Host app: Claude Code (Ink-based TUI)
Analysis (hypothesis, not yet confirmed by instrumentation)
The symptom shape (sparse characters at wide column positions within otherwise-blank rows) is consistent with psmux snapshotting the vt100 parser between successive reader.read() calls that together form a single logical Ink frame.
Mechanism:
- Ink emits cursor-positioned partial updates. A logical frame can be a sequence of
CUP(r,c1) → text → CUP(r,c2) → text → …, often preceded by a row-clear. These sequences can easily exceed the reader's 64KB buffer and get split across multiple read() calls.
- psmux's reader holds the parser mutex for one
read() at a time (src/pane.rs:1137–1154). Between lock acquisitions, the server's snapshot code (src/layout.rs:620–741) can grab the parser mutex and serialize whatever partial state exists. That partial state is emitted as a full dump_buf JSON frame.
- The partial frame is sent to the client and rendered via ratatui, latching the sparse-cell state on screen until a subsequent correct frame is produced and rendered.
Why psmux and not the bare terminal: a raw terminal has no snapshot boundary — Ink's partial writes hit the real cell grid and are overwritten by the next read nanoseconds later. psmux's snapshot+push architecture introduces a discrete boundary that can freeze a mid-sequence state and display it as a finished frame. Claude Code emits valid VT sequences either way; psmux is what can latch them.
Suggested investigation / fix direction
- Instrumentation first. Log each snapshot's timestamp and time since the reader last called
parser.process(). If "scrambled" frames correlate with snapshots that land <1–2 ms after a reader wake, the race is confirmed.
- Minimal fix candidate (reader-side drain). Have the reader drain all currently-available bytes under a single lock hold, so multi-chunk Ink frames land atomically. Needs a non-blocking availability check on the ConPTY pipe, plus a cap on drained bytes per lock hold to avoid starving snapshots under sustained output.
Related
Summary
Terminal output inside a psmux pane is rendered with most cells on some rows cleared, leaving only a sparse set of characters scattered at wide column positions. Other rows on the same screen render fully and correctly. This appears visually similar to #73, but reproduces locally — no SSH involved — so the mangling is not purely an ANSI/SSH-transport issue.
Observed inside Claude Code (React Ink TUI). Not reproducible on demand; exact trigger unknown.
Screenshot
Environment
Analysis (hypothesis, not yet confirmed by instrumentation)
The symptom shape (sparse characters at wide column positions within otherwise-blank rows) is consistent with psmux snapshotting the vt100 parser between successive
reader.read()calls that together form a single logical Ink frame.Mechanism:
CUP(r,c1) → text → CUP(r,c2) → text → …, often preceded by a row-clear. These sequences can easily exceed the reader's 64KB buffer and get split across multipleread()calls.read()at a time (src/pane.rs:1137–1154). Between lock acquisitions, the server's snapshot code (src/layout.rs:620–741) can grab the parser mutex and serialize whatever partial state exists. That partial state is emitted as a fulldump_bufJSON frame.Why psmux and not the bare terminal: a raw terminal has no snapshot boundary — Ink's partial writes hit the real cell grid and are overwritten by the next read nanoseconds later. psmux's snapshot+push architecture introduces a discrete boundary that can freeze a mid-sequence state and display it as a finished frame. Claude Code emits valid VT sequences either way; psmux is what can latch them.
Suggested investigation / fix direction
parser.process(). If "scrambled" frames correlate with snapshots that land <1–2 ms after a reader wake, the race is confirmed.Related
sshFrom Windows To Windows Output Managled #73 — similar visual symptom, reported over SSH; this report confirms the same class of corruption reproduces locally.