Skip to content

Output rendering artifact: rows render with most cells cleared, leaving sparse characters at wide column positions (local, not SSH) #246

@tarikguney

Description

@tarikguney

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

Image

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:

  1. 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.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions